From d2c0893b557fedd9f318a224c89df9a48ec71f4f Mon Sep 17 00:00:00 2001 From: Chloe Yeo Date: Mon, 6 Oct 2025 14:47:47 -0700 Subject: [PATCH 1/5] rdar://149574231 (Make ProgressReporter API) (#3296) --- Package.swift | 5 +- Sources/FoundationEssentials/CMakeLists.txt | 1 + .../ProgressManager/CMakeLists.txt | 23 + .../ProgressManager/ProgressFraction.swift | 345 ++++ .../ProgressManager+Interop.swift | 325 ++++ ...ProgressManager+Properties+Accessors.swift | 525 ++++++ ...ogressManager+Properties+Definitions.swift | 277 +++ .../ProgressManager+Properties+Helpers.swift | 436 +++++ .../ProgressManager+State.swift | 1676 +++++++++++++++++ .../ProgressManager/ProgressManager.swift | 567 ++++++ .../ProgressManager/ProgressReporter.swift | 273 +++ .../ProgressManager/Subprogress.swift | 83 + .../ProgressFractionTests.swift | 167 ++ .../ProgressManagerInteropTests.swift | 332 ++++ .../ProgressManagerPropertiesTests.swift | 1190 ++++++++++++ .../ProgressManagerTests.swift | 795 ++++++++ .../ProgressReporterTests.swift | 150 ++ 17 files changed, 7168 insertions(+), 2 deletions(-) create mode 100644 Sources/FoundationEssentials/ProgressManager/CMakeLists.txt create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressManager.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift create mode 100644 Sources/FoundationEssentials/ProgressManager/Subprogress.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift create mode 100644 Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift diff --git a/Package.swift b/Package.swift index a2b1ccf4b..49922ca56 100644 --- a/Package.swift +++ b/Package.swift @@ -140,7 +140,8 @@ let package = Package( "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", "URL/CMakeLists.txt", - "NotificationCenter/CMakeLists.txt" + "NotificationCenter/CMakeLists.txt", + "ProgressManager/CMakeLists.txt", ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) @@ -185,7 +186,7 @@ let package = Package( "Locale/CMakeLists.txt", "Calendar/CMakeLists.txt", "CMakeLists.txt", - "Predicate/CMakeLists.txt" + "Predicate/CMakeLists.txt", ], cSettings: wasiLibcCSettings, swiftSettings: [ diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index a5a1e9c79..533238baa 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -45,6 +45,7 @@ add_subdirectory(Locale) add_subdirectory(NotificationCenter) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) +add_subdirectory(ProgressManager) add_subdirectory(PropertyList) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt new file mode 100644 index 000000000..325b81e2b --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/CMakeLists.txt @@ -0,0 +1,23 @@ +##===----------------------------------------------------------------------===## +## +## This source file is part of the Swift open source project +## +## Copyright (c) 2025 Apple Inc. and the Swift project authors +## Licensed under Apache License v2.0 +## +## See LICENSE.txt for license information +## See CONTRIBUTORS.md for the list of Swift project authors +## +## SPDX-License-Identifier: Apache-2.0 +## +##===----------------------------------------------------------------------===## +target_sources(FoundationEssentials PRIVATE + ProgressFraction.swift + ProgressManager.swift + ProgressManager+Interop.swift + ProgressManager+Properties+Accessors.swift + ProgressManager+Properties+Definitions.swift + ProgressManager+Properties+Helpers.swift + ProgressManager+State.swift + ProgressReporter.swift + Subprogress.swift) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift new file mode 100644 index 000000000..09372289c --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift @@ -0,0 +1,345 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#endif + +internal struct ProgressFraction : Sendable, Equatable, CustomDebugStringConvertible { + var completed : Int + var total : Int? + /// Indicates whether mathematical operations on this fraction have exceeded integer limits, + /// causing the fraction to fall back to floating-point representation for accuracy. + private(set) var overflowed : Bool + + init() { + completed = 0 + total = nil + overflowed = false + } + + init(double: Double, overflow: Bool = false) { + if double == 0 { + self.completed = 0 + self.total = 1 + } else if double == 1 { + self.completed = 1 + self.total = 1 + } else { + (self.completed, self.total) = ProgressFraction._fromDouble(double) + } + self.overflowed = overflow + } + + init(completed: Int, total: Int?) { + self.total = total + self.completed = completed + self.overflowed = false + } + + // ---- + +#if FOUNDATION_FRAMEWORK + // Glue code for _NSProgressFraction and ProgressFraction + init(nsProgressFraction: _NSProgressFraction) { + self.init(completed: Int(nsProgressFraction.completed), total: Int(nsProgressFraction.total)) + } +#endif + + internal mutating func simplify() { + guard let total = self.total, total != 0 else { + return + } + + (self.completed, self.total) = ProgressFraction._simplify(completed, total) + } + + internal func simplified() -> ProgressFraction? { + if let total = self.total { + let simplified = ProgressFraction._simplify(completed, total) + return ProgressFraction(completed: simplified.0, total: simplified.1) + } else { + return nil + } + } + + /// A closure that performs floating-point arithmetic operations + private typealias FloatingPointOperation = (_ lhs: Double, _ rhs: Double) -> Double + + /// A closure that performs integer arithmetic operations with overflow detection + private typealias OverflowReportingOperation = (_ lhs: Int, _ rhs: Int) -> (Int, overflow: Bool) + + static private func _math(lhs: ProgressFraction, rhs: ProgressFraction, operation: FloatingPointOperation, overflowOperation: OverflowReportingOperation) -> ProgressFraction { + // Mathematically, it is nonsense to add or subtract something with a denominator of 0. However, for the purposes of implementing Progress' fractions, we just assume that a zero-denominator fraction is "weightless" and return the other value. We still need to check for the case where they are both nonsense though. + precondition(!(lhs.total == 0 && rhs.total == 0), "Attempt to add or subtract invalid fraction") + guard let lhsTotal = lhs.total, lhsTotal != 0 else { + return rhs + } + guard let rhsTotal = rhs.total, rhsTotal != 0 else { + return lhs + } + + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + if let lcm = _leastCommonMultiple(lhsTotal, rhsTotal) { + let result = overflowOperation(lhs.completed * (lcm / lhsTotal), rhs.completed * (lcm / rhsTotal)) + if result.overflow { + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Overflow - simplify and then try again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + // Simplification failed, fall back to double math + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + if let lcm = _leastCommonMultiple(lhsSimplifiedTotal, rhsSimplifiedTotal) { + let result = overflowOperation(lhsSimplified.completed * (lcm / lhsSimplifiedTotal), rhsSimplified.completed * (lcm / rhsSimplifiedTotal)) + if result.overflow { + // Use original lhs/rhs here + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Still overflow + return ProgressFraction(double: operation(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + } + } + + static internal func +(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, operation: +, overflowOperation: { $0.addingReportingOverflow($1) }) + } + + static internal func -(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, operation: -, overflowOperation: { $0.subtractingReportingOverflow($1) }) + } + + static internal func *(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction? { + guard !lhs.overflowed && !rhs.overflowed else { + // If either has overflowed already, we preserve that + return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } + + guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else { + return nil + } + + let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed) + let newTotal = lhsTotal.multipliedReportingOverflow(by: rhsTotal) + + if newCompleted.overflow || newTotal.overflow { + // Try simplifying, then do it again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + return nil + } + + let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed) + let newTotalSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplifiedTotal) + + if newCompletedSimplified.overflow || newTotalSimplified.overflow { + // Still overflow + return ProgressFraction(double: lhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } else { + return ProgressFraction(completed: newCompletedSimplified.0, total: newTotalSimplified.0) + } + } else { + return ProgressFraction(completed: newCompleted.0, total: newTotal.0) + } + } + + static internal func /(lhs: ProgressFraction, rhs: Int) -> ProgressFraction? { + guard !lhs.overflowed else { + // If lhs has overflowed, we preserve that + return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } + + guard let lhsTotal = lhs.total else { + return nil + } + + let newTotal = lhsTotal.multipliedReportingOverflow(by: rhs) + + if newTotal.overflow { + let simplified = lhs.simplified() + + guard let simplified = simplified, + let simplifiedTotal = simplified.total else { + return nil + } + + let newTotalSimplified = simplifiedTotal.multipliedReportingOverflow(by: rhs) + + if newTotalSimplified.overflow { + // Still overflow + return ProgressFraction(double: lhs.fractionCompleted / Double(rhs), overflow: true) + } else { + return ProgressFraction(completed: lhs.completed, total: newTotalSimplified.0) + } + } else { + return ProgressFraction(completed: lhs.completed, total: newTotal.0) + } + } + + static internal func ==(lhs: ProgressFraction, rhs: ProgressFraction) -> Bool { + if lhs.isNaN || rhs.isNaN { + // NaN fractions are never equal + return false + } else if lhs.total == rhs.total { + // Direct comparison of numerator + return lhs.completed == rhs.completed + } else if lhs.total == nil && rhs.total != nil { + return false + } else if lhs.total != nil && rhs.total == nil { + return false + } else if lhs.completed == 0 && rhs.completed == 0 { + return true + } else if lhs.completed == lhs.total && rhs.completed == rhs.total { + // Both finished (1) + return true + } else if (lhs.completed == 0 && rhs.completed != 0) || (lhs.completed != 0 && rhs.completed == 0) { + // One 0, one not 0 + return false + } else { + // Cross-multiply + guard let lhsTotal = lhs.total, let rhsTotal = rhs.total else { + return false + } + + let left = lhs.completed.multipliedReportingOverflow(by: rhsTotal) + let right = lhsTotal.multipliedReportingOverflow(by: rhs.completed) + + if !left.overflow && !right.overflow { + if left.0 == right.0 { + return true + } + } else { + // Try simplifying then cross multiply again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + guard let lhsSimplified = lhsSimplified, + let rhsSimplified = rhsSimplified, + let lhsSimplifiedTotal = lhsSimplified.total, + let rhsSimplifiedTotal = rhsSimplified.total else { + // Simplification failed, fall back to doubles + return lhs.fractionCompleted == rhs.fractionCompleted + } + + let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplifiedTotal) + let rightSimplified = lhsSimplifiedTotal.multipliedReportingOverflow(by: rhsSimplified.completed) + + if !leftSimplified.overflow && !rightSimplified.overflow { + if leftSimplified.0 == rightSimplified.0 { + return true + } + } else { + // Ok... fallback to doubles. This doesn't use an epsilon + return lhs.fractionCompleted == rhs.fractionCompleted + } + } + } + + return false + } + + // ---- + + internal var isFinished: Bool { + guard let total else { + return false + } + return completed >= total && completed > 0 && total > 0 + } + + internal var isIndeterminate: Bool { + return total == nil + } + + + internal var fractionCompleted : Double { + guard let total else { + return 0.0 + } + return Double(completed) / Double(total) + } + + + internal var isNaN : Bool { + return total == 0 + } + + internal var debugDescription : String { + return "\(completed) / \(total) (\(fractionCompleted)), overflowed: \(overflowed)" + } + + // ---- + + private static func _fromDouble(_ d : Double) -> (Int, Int) { + // This simplistic algorithm could someday be replaced with something better. + // Basically - how many 1/Nths is this double? + var denominator: Int + switch Int.bitWidth { + case 32: denominator = 1048576 // 2^20 - safe for 32-bit + case 64: denominator = 1073741824 // 2^30 - high precision for 64-bit + default: denominator = 131072 // 2^17 - ultra-safe fallback + } + let numerator = Int(d / (1.0 / Double(denominator))) + return (numerator, denominator) + } + + private static func _greatestCommonDivisor(_ inA : Int, _ inB : Int) -> Int { + // This is Euclid's algorithm. There are faster ones, like Knuth, but this is the simplest one for now. + var a = inA + var b = inB + repeat { + let tmp = b + b = a % b + a = tmp + } while (b != 0) + return a + } + + private static func _leastCommonMultiple(_ a : Int, _ b : Int) -> Int? { + // This division always results in an integer value because gcd(a,b) is a divisor of a. + // lcm(a,b) == (|a|/gcd(a,b))*b == (|b|/gcd(a,b))*a + let result = (a / _greatestCommonDivisor(a, b)).multipliedReportingOverflow(by: b) + if result.overflow { + return nil + } else { + return result.0 + } + } + + private static func _simplify(_ n : Int, _ d : Int) -> (Int, Int) { + let gcd = _greatestCommonDivisor(n, d) + return (n / gcd, d / gcd) + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift new file mode 100644 index 000000000..610aaf0aa --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -0,0 +1,325 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if FOUNDATION_FRAMEWORK +internal import _ForSwiftFoundation +#if canImport(Synchronization) +internal import Synchronization +#endif + +//MARK: Progress Parent - Subprogress / ProgressReporter Child Interop +@available(FoundationPreview 6.4, *) +extension Progress { + + /// Returns a Subprogress which can be passed to any method that reports progress + /// It can be then used to create a child `ProgressManager` reporting to this `Progress` + /// + /// Delegates a portion of totalUnitCount to a future child `ProgressManager` instance. + /// + /// - Parameter count: Number of units delegated to a child instance of `ProgressManager` + /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Returns: A `Subprogress` instance. + public func makeChild(withPendingUnitCount count: Int) -> Subprogress { + + // Make a ProgressManager + let manager = ProgressManager(totalCount: 1) + + // Create a NSProgress - ProgressManager bridge for mirroring + let subprogressBridge = SubprogressBridge( + parent: self, + portion: Int64(count), + manager: manager + ) + + // Instantiate a Subprogress with ProgressManager as parent + // Store bridge + let subprogress = Subprogress( + parent: manager, + assignedCount: 1, + subprogressBridge: subprogressBridge + ) + + return subprogress + } + + /// Adds a ProgressReporter as a child to a Progress, which constitutes a portion of Progress's totalUnitCount. + /// + /// - Parameters: + /// - reporter: A `ProgressReporter` instance. + /// - count: Number of units delegated from `self`'s `totalCount`. + public func addChild(_ reporter: ProgressReporter, withPendingUnitCount count: Int) { + + precondition(self.isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + + // Create a NSProgress - ProgressReporter bridge + let reporterBridge = ProgressReporterBridge( + parent: self, + portion: Int64(count), + reporterBridge: reporter + ) + + // Store bridge + reporter.manager.addBridge(reporterBridge: reporterBridge) + } + + // MARK: Cycle detection + private func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + guard let parent = self._parent() else { + return false + } + + guard parent is NSProgressBridge else { + return parent.isCycle(reporter: reporter) + } + + guard let unwrappedParent = (parent as? NSProgressBridge)?.manager else { + return false + } + + if unwrappedParent === reporter.manager { + return true + } + + let updatedVisited = visited.union([unwrappedParent]) + return unwrappedParent.isCycleInterop(reporter: reporter, visited: updatedVisited) + } +} + +@available(FoundationPreview 6.4, *) +//MARK: ProgressManager Parent - Progress Child Interop +extension ProgressManager { + + /// Adds a Foundation's `Progress` instance as a child which constitutes a certain `count` of `self`'s `totalCount`. + /// - Parameters: + /// - count: Number of units delegated from `self`'s `totalCount`. + /// - progress: `Progress` which receives the delegated `count`. + public func subprogress(assigningCount count: Int, to progress: Foundation.Progress) { + precondition(progress._parent() == nil, "Cannot assign a progress to more than one parent.") + + // Create a ProgressManager - NSProgress bridge + let progressBridge = NSProgressBridge( + manager: self, + progress: progress, + assignedCount: count + ) + + // Add bridge as a parent + progress._setParent(progressBridge, portion: Int64(count)) + + // Store bridge + self.addBridge(nsProgressBridge: progressBridge) + } +} + +@available(FoundationPreview 6.4, *) +internal final class SubprogressBridge: Sendable { + + internal let progressBridge: Progress + internal let manager: ProgressManager + + init(parent: Progress, portion: Int64, manager: ProgressManager) { + self.progressBridge = Progress(totalUnitCount: 1, parent: parent, pendingUnitCount: portion) + self.manager = manager + + manager.addObserver { [weak self] observerState in + guard let self else { + return + } + + // This needs to change totalUnitCount before completedUnitCount otherwise progressBridge will finish and mess up the math + self.progressBridge.totalUnitCount = Int64(observerState.totalCount) + self.progressBridge.completedUnitCount = Int64(observerState.completedCount) + } + } +} + +@available(FoundationPreview 6.4, *) +internal final class ProgressReporterBridge: Sendable { + + internal let progressBridge: Progress + internal let reporterBridge: ProgressReporter + + init(parent: Progress, portion: Int64, reporterBridge: ProgressReporter) { + self.progressBridge = Progress( + totalUnitCount: Int64(reporterBridge.manager.totalCount ?? 0), + parent: parent, + pendingUnitCount: portion + ) + self.progressBridge.completedUnitCount = Int64(reporterBridge.manager.completedCount) + self.reporterBridge = reporterBridge + + let manager = reporterBridge.manager + + manager.addObserver { [weak self] observerState in + guard let self else { + return + } + + self.progressBridge.totalUnitCount = Int64(observerState.totalCount) + self.progressBridge.completedUnitCount = Int64(observerState.completedCount) + } + } + +} + +@available(FoundationPreview 6.4, *) +internal final class NSProgressBridge: Progress, @unchecked Sendable { + + internal let manager: ProgressManager + internal let managerBridge: ProgressManager + internal let progress: Progress + + init(manager: ProgressManager, progress: Progress, assignedCount: Int) { + self.manager = manager + self.managerBridge = ProgressManager(totalCount: Int(clamping: progress.totalUnitCount)) + self.progress = progress + super.init(parent: nil, userInfo: nil) + + managerBridge.setCounts { completed, total in + completed = Int(clamping: progress.completedUnitCount) + } + + let position = manager.addChild( + childManager: managerBridge, + assignedCount: assignedCount, + childFraction: ProgressFraction(completed: Int(clamping: completedUnitCount), total: Int(clamping: totalUnitCount)) + ) + managerBridge.addParent(parentManager: manager, positionInParent: position) + } + + // Overrides the _updateChild func that Foundation.Progress calls to update parent + // so that the parent that gets updated is the ProgressManager parent + override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { + managerBridge.setCounts { completed, total in + completed = Int(clamping: fraction.next.completed) + total = Int(clamping: fraction.next.total) + } + managerBridge.markSelfDirty() + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + // Keeping this as an enum in case we have other states to track in the future. + internal struct ObserverState { + var totalCount: Int + var completedCount: Int + } + + internal struct InteropObservation { + let subprogressBridge: SubprogressBridge? + var reporterBridge: ProgressReporterBridge? + var nsProgressBridge: Foundation.Progress? + } + + internal enum InteropType { + case interopMirror(ProgressManager) + case interopObservation(InteropObservation) + + internal var totalCount: Int? { + switch self { + case .interopMirror(let mirror): + mirror.totalCount + case .interopObservation: + nil + } + } + + internal var completedCount: Int? { + switch self { + case .interopMirror(let mirror): + mirror.completedCount + case .interopObservation: + nil + } + } + + internal var fractionCompleted: Double? { + switch self { + case .interopMirror(let mirror): + mirror.fractionCompleted + case .interopObservation: + nil + } + } + + internal var isIndeterminate: Bool? { + switch self { + case .interopMirror(let mirror): + mirror.isIndeterminate + case .interopObservation: + nil + } + } + + internal var isFinished: Bool? { + switch self { + case .interopMirror(let mirror): + mirror.isFinished + case .interopObservation: + nil + } + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager.State { + internal func notifyObservers(with observerState: ProgressManager.ObserverState) { + for observer in observers { + observer(observerState) + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + //MARK: Interop Methods + /// Adds `observer` to list of `_observers` in `self`. + internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { + state.withLock { state in + state.observers.append(observer) + } + } + + /// Notifies all `_observers` of `self` when `state` changes. + internal func notifyObservers(with observedState: ObserverState) { + state.withLock { state in + for observer in state.observers { + observer(observedState) + } + } + } + + internal func addBridge(reporterBridge: ProgressReporterBridge? = nil, nsProgressBridge: Foundation.Progress? = nil) { + state.withLock { state in + var interopObservation = InteropObservation(subprogressBridge: nil) + + if let reporterBridge { + interopObservation.reporterBridge = reporterBridge + } + + if let nsProgressBridge { + interopObservation.nsProgressBridge = nsProgressBridge + } + + state.interopType = .interopObservation(interopObservation) + } + } + + internal func setInteropChild(interopMirror: ProgressManager) { + state.withLock { state in + state.interopType = .interopMirror(interopMirror) + } + } +} +#endif diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift new file mode 100644 index 000000000..ca5448260 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -0,0 +1,525 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + // MARK: Methods to Read & Write Custom Properties of single ProgressManager node + /// Gets or sets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int where P.Value == Int, P.Summary == Int { + get { + if P.self == ProgressManager.Properties.TotalFileCount.self { + self.access(keyPath: \.totalFileCount) + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + self.access(keyPath: \.completedFileCount) + } else { + self.access(keyPath: \.customPropertiesInt) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.TotalFileCount.self { + return state.totalFileCount + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + return state.completedFileCount + } else { + return state.customPropertiesInt[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.TotalFileCount.self { + self.withMutation(keyPath: \.totalFileCount) { + parents = state.withLock { state in + guard newValue != state.totalFileCount else { + return nil + } + state.totalFileCount = newValue + return state.parents + } + } + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + self.withMutation(keyPath: \.completedFileCount) { + parents = state.withLock { state in + guard newValue != state.completedFileCount else { + return nil + } + state.completedFileCount = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesInt) { + parents = state.withLock { state in + guard newValue != state.customPropertiesInt[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesInt[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.TotalFileCount.self { + markSelfDirty(property: ProgressManager.Properties.TotalFileCount.self, parents: parents) + } else if P.self == ProgressManager.Properties.CompletedFileCount.self { + markSelfDirty(property: ProgressManager.Properties.CompletedFileCount.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + get { + if P.self == ProgressManager.Properties.TotalByteCount.self { + self.access(keyPath: \.totalByteCount) + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + self.access(keyPath: \.completedByteCount) + } else { + self.access(keyPath: \.customPropertiesUInt64) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.TotalByteCount.self { + return state.totalByteCount + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + return state.completedByteCount + } else { + return state.customPropertiesUInt64[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.TotalByteCount.self { + self.withMutation(keyPath: \.totalByteCount) { + parents = state.withLock { state in + guard newValue != state.totalByteCount else { + return nil + } + state.totalByteCount = newValue + return state.parents + } + } + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + self.withMutation(keyPath: \.completedByteCount) { + parents = state.withLock { state in + guard newValue != state.completedByteCount else { + return nil + } + state.completedByteCount = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesUInt64) { + parents = state.withLock { state in + guard newValue != state.customPropertiesUInt64[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesUInt64[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.TotalByteCount.self { + markSelfDirty(property: ProgressManager.Properties.TotalByteCount.self, parents: parents) + } else if P.self == ProgressManager.Properties.CompletedByteCount.self { + markSelfDirty(property: ProgressManager.Properties.CompletedByteCount.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> P.Value where P.Value == Double, P.Summary == Double { + get { + self.access(keyPath: \.customPropertiesDouble) + return state.withLock { state in + return state.customPropertiesDouble[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesDouble) { + parents = state.withLock { state in + guard newValue != state.customPropertiesDouble[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesDouble[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { + get { + self.access(keyPath: \.customPropertiesString) + return state.withLock { state in + return state.customPropertiesString[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesString) { + parents = state.withLock { state in + guard newValue != state.customPropertiesString[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesString[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { + get { + self.access(keyPath: \.customPropertiesURL) + return state.withLock { state in + return state.customPropertiesURL[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + + set { + var parents: [Parent]? + self.withMutation(keyPath: \.customPropertiesURL) { + parents = state.withLock { state in + guard newValue != state.customPropertiesURL[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesURL[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + + if let parents = parents { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == [UInt64] { + get { + if P.self == ProgressManager.Properties.Throughput.self { + self.access(keyPath: \.throughput) + } else { + self.access(keyPath: \.customPropertiesUInt64Array) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.Throughput.self { + return state.throughput + } else { + return state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.Throughput.self { + self.withMutation(keyPath: \.throughput) { + parents = state.withLock { state in + guard newValue != state.throughput else { + return nil + } + state.throughput = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesUInt64Array) { + parents = state.withLock { state in + guard newValue != state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesUInt64Array[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.Throughput.self { + markSelfDirty(property: ProgressManager.Properties.Throughput.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + /// Gets or sets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `Duration` and the summary type is `Duration`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { + get { + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.access(keyPath: \.estimatedTimeRemaining) + } else { + self.access(keyPath: \.customPropertiesDuration) + } + return state.withLock { state in + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + return state.estimatedTimeRemaining + } else { + return state.customPropertiesDuration[MetatypeWrapper(P.self)] ?? P.defaultValue + } + } + } + + set { + var parents: [Parent]? + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.withMutation(keyPath: \.estimatedTimeRemaining) { + parents = state.withLock { state in + guard newValue != state.estimatedTimeRemaining else { + return nil + } + state.estimatedTimeRemaining = newValue + return state.parents + } + } + } else { + self.withMutation(keyPath: \.customPropertiesDuration) { + parents = state.withLock { state in + guard newValue != state.customPropertiesDuration[MetatypeWrapper(P.self)] else { + return nil + } + state.customPropertiesDuration[MetatypeWrapper(P.self)] = newValue + return state.parents + } + } + } + + if let parents = parents { + if P.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + markSelfDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.self, parents: parents) + } else { + markSelfDirty(property: MetatypeWrapper(P.self), parents: parents) + } + } + } + } + + // MARK: Methods to Read Custom Properties of Subtree with ProgressManager as root + + + /// Returns a summary for a custom integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: An `Int` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Int, P.Summary == Int { + if property.self == ProgressManager.Properties.TotalFileCount.self { + self.access(keyPath: \.totalFileCount) + self.access(keyPath: \.totalFileCountSummary) + self.didSet(keyPath: \.totalFileCountSummary) + return updatedFileCount(type: .total) + } else if property.self == ProgressManager.Properties.CompletedFileCount.self { + self.access(keyPath: \.completedFileCount) + self.access(keyPath: \.completedFileCountSummary) + self.didSet(keyPath: \.completedFileCountSummary) + return updatedFileCount(type: .completed) + } else { + self.access(keyPath: \.customPropertiesInt) + self.access(keyPath: \.customPropertiesIntSummary) + self.didSet(keyPath: \.customPropertiesIntSummary) + return updatedIntSummary(property: MetatypeWrapper(property)) + } + } + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: An `UInt64` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == UInt64, P.Summary == UInt64 { + if property.self == ProgressManager.Properties.TotalByteCount.self { + self.access(keyPath: \.totalByteCount) + self.access(keyPath: \.totalByteCountSummary) + self.didSet(keyPath: \.totalByteCountSummary) + return updatedByteCount(type: .total) + } else if property.self == ProgressManager.Properties.CompletedByteCount.self { + self.access(keyPath: \.completedByteCount) + self.access(keyPath: \.completedByteCountSummary) + self.didSet(keyPath: \.completedByteCountSummary) + return updatedByteCount(type: .completed) + } else { + self.access(keyPath: \.customPropertiesUInt64) + self.access(keyPath: \.customPropertiesUInt64Summary) + self.didSet(keyPath: \.customPropertiesUInt64Summary) + return updatedUInt64Summary(property: MetatypeWrapper(property)) + } + } + + /// Returns a summary for a custom double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: A `Double` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Double, P.Summary == Double { + self.access(keyPath: \.customPropertiesDouble) + self.access(keyPath: \.customPropertiesDoubleSummary) + self.didSet(keyPath: \.customPropertiesDoubleSummary) + return updatedDoubleSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for a custom string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value type is `String?` and the summary type is `[String?]`. + /// - Returns: A `[String?]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == String?, P.Summary == [String?] { + self.access(keyPath: \.customPropertiesString) + self.access(keyPath: \.customPropertiesStringSummary) + self.didSet(keyPath: \.customPropertiesStringSummary) + return updatedStringSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for a custom URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from this progress manager + /// and all its children, returning a consolidated summary value as an array of URLs. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where the value type is `URL?` and the summary type is `[URL?]`. + /// - Returns: A `[URL?]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == URL?, P.Summary == [URL?] { + self.access(keyPath: \.customPropertiesURL) + self.access(keyPath: \.customPropertiesURLSummary) + self.didSet(keyPath: \.customPropertiesURLSummary) + return updatedURLSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for a custom unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from this progress manager + /// and all its children, returning a consolidated summary value as an array of UInt64 values. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: A `[UInt64]` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == UInt64, P.Summary == [UInt64] { + if property.self == ProgressManager.Properties.Throughput.self { + self.access(keyPath: \.throughput) + self.access(keyPath: \.throughputSummary) + self.didSet(keyPath: \.throughputSummary) + return updatedThroughput() + } else { + self.access(keyPath: \.customPropertiesUInt64Array) + self.access(keyPath: \.customPropertiesUInt64ArraySummary) + self.didSet(keyPath: \.customPropertiesUInt64ArraySummary) + return updatedUInt64ArraySummary(property: MetatypeWrapper(property)) + } + } + + /// Returns a summary for a custom duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from this progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where the value type is `Duration` and the summary type is `Duration`. + /// - Returns: A `Duration` summary value for the specified property. + public func summary(of property: P.Type) -> P.Summary where P.Value == Duration, P.Summary == Duration { + if property.self == ProgressManager.Properties.EstimatedTimeRemaining.self { + self.access(keyPath: \.estimatedTimeRemaining) + self.access(keyPath: \.estimatedTimeRemainingSummary) + self.didSet(keyPath: \.estimatedTimeRemainingSummary) + return updatedEstimatedTimeRemaining() + } else { + self.access(keyPath: \.customPropertiesDuration) + self.access(keyPath: \.customPropertiesDurationSummary) + self.didSet(keyPath: \.customPropertiesDurationSummary) + return updatedDurationSummary(property: MetatypeWrapper(property)) + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift new file mode 100644 index 000000000..cbdb45854 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -0,0 +1,277 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + /// A type that conveys additional task-specific information on progress. + /// + /// The `Property` protocol defines custom properties that can be associated with progress tracking. + /// These properties allow you to store and aggregate additional information alongside the + /// standard progress metrics such as `totalCount` and `completedCount`. + public protocol Property: SendableMetatype { + + /// The type used for individual values of this property. + /// + /// This associated type represents the type of property values + /// that can be set on progress managers. Must be `Sendable` and `Equatable`. + /// The currently allowed types are `Int`, `Double`, `String?`, `URL?` or `UInt64`. + associatedtype Value: Sendable, Equatable + + /// The type used for aggregated summaries of this property. + /// + /// This associated type represents the type used when summarizing property values + /// across multiple progress managers in a subtree. + /// The currently allowed types are `Int`, `Double`, `[String?]`, `[URL?]` or `[UInt64]`. + associatedtype Summary: Sendable, Equatable + + /// A unique identifier for this property type. + /// + /// The key should use reverse DNS style notation to ensure uniqueness across different + /// frameworks and applications. + /// + /// - Returns: A unique string identifier for this property type. + static var key: String { get } + + /// The default value to return when property is not set to a specific value. + /// + /// This value is used when a progress manager doesn't have an explicit value set + /// for this property type. + /// + /// - Returns: The default value for this property type. + static var defaultValue: Value { get } + + /// The default summary value for this property type. + /// + /// This value is used as the initial summary when no property values have been + /// aggregated yet. + /// + /// - Returns: The default summary value for this property type. + static var defaultSummary: Summary { get } + + /// Reduces a property value into an accumulating summary. + /// + /// This method is called to incorporate individual property values into a summary + /// that represents the aggregated state across multiple progress managers. + /// + /// - Parameters: + /// - summary: The accumulating summary value to modify. + /// - value: The individual property value to incorporate into the summary. + static func reduce(into summary: inout Summary, value: Value) + + /// Merges two summary values into a single combined summary. + /// + /// This method is called to combine summary values from different branches + /// of the progress manager hierarchy into a unified summary. + /// + /// - Parameters: + /// - summary1: The first summary to merge. + /// - summary2: The second summary to merge. + /// - Returns: A new summary that represents the combination of both input summaries. + static func merge(_ summary1: Summary, _ summary2: Summary) -> Summary + + /// Determines how to handle summary data when a progress manager is deinitialized. + /// + /// This method is used when a progress manager in the hierarchy is being + /// deinitialized and its accumulated summary needs to be processed in relation to + /// its parent's summary. The behavior can vary depending on the property type: + /// + /// - For additive properties (like file counts, byte counts): The self summary + /// is typically added to the parent summary to preserve the accumulated progress. + /// - For max-based properties (like estimated time remaining): The parent summary + /// is typically preserved as it represents an existing estimate. + /// - For collection-based properties (like file URLs): The self summary may be + /// discarded to avoid accumulating stale references. + /// + /// - Parameters: + /// - parentSummary: The current summary value of the parent progress manager. + /// - selfSummary: The final summary value from the progress manager being deinitialized. + /// - Returns: The updated summary that replaces the parent's current summary. + static func finalSummary(_ parentSummary: Summary, _ selfSummary: Summary) -> Summary + } + + // Namespace for properties specific to operations reported on + @frozen + public enum Properties: Sendable { + + /// The total number of files. + public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } + @frozen + public enum TotalFileCount: Sendable, Property { + + public typealias Value = Int + + public typealias Summary = Int + + public static var key: String { return "Foundation.ProgressManager.Properties.TotalFileCount" } + + public static var defaultValue: Int { return 0 } + + public static var defaultSummary: Int { return 0 } + + public static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary + } + } + + /// The number of completed files. + public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } + @frozen + public enum CompletedFileCount: Sendable, Property { + + public typealias Value = Int + + public typealias Summary = Int + + public static var key: String { return "Foundation.ProgressManager.Properties.CompletedFileCount" } + + public static var defaultValue: Int { return 0 } + + public static var defaultSummary: Int { return 0 } + + public static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + public static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary + } + } + + /// The total number of bytes. + public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } + @frozen + public enum TotalByteCount: Sendable, Property { + + public typealias Value = UInt64 + + public typealias Summary = UInt64 + + public static var key: String { return "Foundation.ProgressManager.Properties.TotalByteCount" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: UInt64 { return 0 } + + public static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary + } + } + + /// The number of completed bytes. + public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } + @frozen + public enum CompletedByteCount: Sendable, Property { + + public typealias Value = UInt64 + + public typealias Summary = UInt64 + + public static var key: String { return "Foundation.ProgressManager.Properties.CompletedByteCount" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: UInt64 { return 0 } + + public static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary + } + } + + /// The throughput, in bytes per second. + public var throughput: Throughput.Type { Throughput.self } + @frozen + public enum Throughput: Sendable, Property { + public typealias Value = UInt64 + + public typealias Summary = [UInt64] + + public static var key: String { return "Foundation.ProgressManager.Properties.Throughput" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: [UInt64] { return [] } + + public static func reduce(into summary: inout [UInt64], value: UInt64) { + summary.append(value) + } + + public static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { + return summary1 + summary2 + } + + public static func finalSummary(_ parentSummary: [UInt64], _ selfSummary: [UInt64]) -> [UInt64] { + return parentSummary + selfSummary + } + } + + /// The amount of time remaining in the processing of files. + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } + @frozen + public enum EstimatedTimeRemaining: Sendable, Property { + + public typealias Value = Duration + + public typealias Summary = Duration + + public static var key: String { return "Foundation.ProgressManager.Properties.EstimatedTimeRemaining" } + + public static var defaultValue: Duration { return Duration.seconds(0) } + + public static var defaultSummary: Duration { return Duration.seconds(0) } + + public static func reduce(into summary: inout Duration, value: Duration) { + if summary >= value { + return + } else { + summary = value + } + } + + public static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration { + return max(summary1, summary2) + } + + public static func finalSummary(_ parentSummary: Duration, _ selfSummary: Duration) -> Duration { + return parentSummary + } + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift new file mode 100644 index 000000000..0317de6a7 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -0,0 +1,436 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + internal enum CountType { + case total + case completed + } + + //MARK: Helper Methods for Updating Dirty Path + internal func updatedIntSummary(property: MetatypeWrapper) -> Int { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getIntSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.IntSummaryUpdate(index: index, updatedSummary: child.updatedIntSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateIntSummary(updateInfo, updatedSummaries) + } + } + + internal func updatedUInt64Summary(property: MetatypeWrapper) -> UInt64 { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getUInt64SummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.UInt64SummaryUpdate(index: index, updatedSummary: child.updatedUInt64Summary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateUInt64Summary(updateInfo, updatedSummaries) + } + } + + internal func updatedDoubleSummary(property: MetatypeWrapper) -> Double { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getDoubleSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.DoubleSummaryUpdate(index: index, updatedSummary: child.updatedDoubleSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateDoubleSummary(updateInfo, updatedSummaries) + } + } + + internal func updatedStringSummary(property: MetatypeWrapper) -> [String?] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getStringSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.StringSummaryUpdate(index: index, updatedSummary: child.updatedStringSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateStringSummary(updateInfo, updatedSummaries) + } + } + + internal func updatedURLSummary(property: MetatypeWrapper) -> [URL?] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getURLSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.URLSummaryUpdate(index: index, updatedSummary: child.updatedURLSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateURLSummary(updateInfo, updatedSummaries) + } + } + + internal func updatedUInt64ArraySummary(property: MetatypeWrapper) -> [UInt64] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getUInt64ArraySummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.UInt64ArraySummaryUpdate(index: index, updatedSummary: child.updatedUInt64ArraySummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateUInt64ArraySummary(updateInfo, updatedSummaries) + } + } + + internal func updatedDurationSummary(property: MetatypeWrapper) -> Duration { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getDurationSummaryUpdateInfo(property: property) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.DurationSummaryUpdate(index: index, updatedSummary: child.updatedDurationSummary(property: property)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateDurationSummary(updateInfo, updatedSummaries) + } + } + + internal func updatedFileCount(type: CountType) -> Int { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getFileCountUpdateInfo(type: type) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.FileCountUpdate(index: index, updatedSummary: child.updatedFileCount(type: type)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateFileCount(updateInfo, updatedSummaries) + } + } + + internal func updatedByteCount(type: CountType) -> UInt64 { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getByteCountUpdateInfo(type: type) + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.ByteCountUpdate(index: index, updatedSummary: child.updatedByteCount(type: type)) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateByteCount(updateInfo, updatedSummaries) + } + } + + internal func updatedThroughput() -> [UInt64] { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getThroughputUpdateInfo() + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.ThroughputUpdate(index: index, updatedSummary: child.updatedThroughput()) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updatedThroughput(updateInfo, updatedSummaries) + } + } + + internal func updatedEstimatedTimeRemaining() -> Duration { + // Get information about dirty children and summaries of non-dirty children + let updateInfo = state.withLock { state in + state.getEstimatedTimeRemainingUpdateInfo() + } + + // Get updated summary for each dirty child + let updatedSummaries = updateInfo.dirtyChildren.map { (index, child) in + State.EstimatedTimeRemainingUpdate(index: index, updatedSummary: child.updatedEstimatedTimeRemaining()) + } + + // Consolidate updated summaries of dirty children and summaries of non-dirty children + return state.withLock { state in + state.updateEstimatedTimeRemaining(updateInfo, updatedSummaries) + } + } + + //MARK: Helper Methods for Setting Dirty Paths + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedFileCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalByteCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedByteCount.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.Throughput.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, parents: [Parent]) { + for parent in parents { + parent.manager.markChildDirty(property: property, at: parent.positionInParent) + } + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesIntSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesUInt64Summary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesDoubleSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesStringSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesURLSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesUInt64ArraySummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + self.willSet(keyPath: \.customPropertiesDurationSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) { + self.willSet(keyPath: \.totalFileCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedFileCount.Type, at position: Int) { + self.willSet(keyPath: \.completedFileCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalByteCount.Type, at position: Int) { + self.willSet(keyPath: \.totalByteCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedByteCount.Type, at position: Int) { + self.willSet(keyPath: \.completedByteCountSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.Throughput.Type, at position: Int) { + self.willSet(keyPath: \.throughputSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, at position: Int) { + self.willSet(keyPath: \.estimatedTimeRemainingSummary) + let parents = state.withLock { state in + state.markChildDirty(property: property, at: position) + } + markSelfDirty(property: property, parents: parents) + } + + //MARK: Method to preserve values of properties upon deinit + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, propertiesInt: [MetatypeWrapper: Int], propertiesUInt64: [MetatypeWrapper: UInt64], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]], propertiesURL: [MetatypeWrapper: [URL?]], propertiesUInt64Array: [MetatypeWrapper: [UInt64]], propertiesDuration: [MetatypeWrapper: Duration]) { + state.withLock { state in + // The children's values are marked as non-dirty because these values are going to be in the leaf nodes. The dirty bit usually signals that there is a need to call helper method child.updatedSummary to iterate through this child's children to get updated values. But since after this level the child is already deinit, that means there is no need to clear dirty bits anymore. + state.children[position].totalFileCountSummary = PropertyStateInt(value: totalFileCount, isDirty: false) + state.children[position].completedFileCountSummary = PropertyStateInt(value: completedFileCount, isDirty: false) + state.children[position].totalByteCountSummary = PropertyStateUInt64(value: totalByteCount, isDirty: false) + state.children[position].completedByteCountSummary = PropertyStateUInt64(value: completedByteCount, isDirty: false) + state.children[position].throughputSummary = PropertyStateUInt64Array(value: throughput, isDirty: false) + state.children[position].estimatedTimeRemainingSummary = PropertyStateDuration(value: estimatedTimeRemaining, isDirty: false) + + for (propertyKey, propertyValue) in propertiesInt { + state.children[position].customPropertiesIntSummary[propertyKey] = PropertyStateInt(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesUInt64 { + state.children[position].customPropertiesUInt64Summary[propertyKey] = PropertyStateUInt64(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesDouble { + state.children[position].customPropertiesDoubleSummary[propertyKey] = PropertyStateDouble(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesString { + state.children[position].customPropertiesStringSummary[propertyKey] = PropertyStateString(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesURL { + state.children[position].customPropertiesURLSummary[propertyKey] = PropertyStateURL(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesUInt64Array { + state.children[position].customPropertiesUInt64ArraySummary[propertyKey] = PropertyStateUInt64Array(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesDuration { + state.children[position].customPropertiesDurationSummary[propertyKey] = PropertyStateDuration(value: propertyValue, isDirty: false) + } + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift new file mode 100644 index 000000000..aa894f09c --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -0,0 +1,1676 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +#if canImport(Synchronization) +internal import Synchronization +#endif + +@available(FoundationPreview 6.4, *) +extension ProgressManager { + + internal struct MetatypeWrapper: Hashable, Equatable, Sendable { + + let reduce: @Sendable (inout S, V) -> () + let merge: @Sendable (S, S) -> S + let finalSummary: @Sendable (S, S) -> S + + let defaultValue: V + let defaultSummary: S + + let key: String + + init(_ argument: P.Type) where P.Value == V, P.Summary == S { + reduce = P.reduce + merge = P.merge + finalSummary = P.finalSummary + defaultValue = P.defaultValue + defaultSummary = P.defaultSummary + key = P.key + } + + func hash(into hasher: inout Hasher) { + hasher.combine(key) + } + + static func == (lhs: ProgressManager.MetatypeWrapper, rhs: ProgressManager.MetatypeWrapper) -> Bool { + lhs.key == rhs.key + } + } + + internal struct PropertyStateInt { + var value: Int + var isDirty: Bool + } + + internal struct PropertyStateUInt64 { + var value: UInt64 + var isDirty: Bool + } + + internal struct PropertyStateUInt64Array { + var value: [UInt64] + var isDirty: Bool + } + + internal struct PropertyStateDuration { + var value: Duration + var isDirty: Bool + } + + internal struct PropertyStateDouble { + var value: Double + var isDirty: Bool + } + + internal struct PropertyStateString { + var value: [String?] + var isDirty: Bool + } + + internal struct PropertyStateURL { + var value: [URL?] + var isDirty: Bool + } + + internal struct PendingChildUpdateInfo { + let index: Int + let manager: ProgressManager + let wasFinished: Bool + let assignedCount: Int + } + + internal struct PendingChildUpdate { + let index: Int + let updatedFraction: ProgressFraction + let assignedCount: Int + } + + internal struct Child { + weak var manager: ProgressManager? + // portion of self's totalCount assigned to child + var assignedCount: Int + var fraction: ProgressFraction + var isFractionDirty: Bool + // Summaries of declared custom properties in subtree + var totalFileCountSummary: PropertyStateInt + var completedFileCountSummary: PropertyStateInt + var totalByteCountSummary: PropertyStateUInt64 + var completedByteCountSummary: PropertyStateUInt64 + var throughputSummary: PropertyStateUInt64Array + var estimatedTimeRemainingSummary: PropertyStateDuration + // Summaries of custom properties declared by developers in subtree + var customPropertiesIntSummary: [MetatypeWrapper: PropertyStateInt] + var customPropertiesUInt64Summary: [MetatypeWrapper: PropertyStateUInt64] + var customPropertiesDoubleSummary: [MetatypeWrapper: PropertyStateDouble] + var customPropertiesStringSummary: [MetatypeWrapper: PropertyStateString] + var customPropertiesURLSummary: [MetatypeWrapper: PropertyStateURL] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: PropertyStateUInt64Array] + var customPropertiesDurationSummary: [MetatypeWrapper: PropertyStateDuration] + } + + internal struct Parent { + var manager: ProgressManager + // self's position in parent's children list for array indexing + var positionInParent: Int + } + + internal struct State { + var selfFraction: ProgressFraction + var overallFraction: ProgressFraction { + // If any child has finished, the assigned count would have been added to selfFraction previously + var overallFraction = selfFraction + for child in children { + // So we only need to check child that has not finished, and include their fraction to overallFraction + if !child.fraction.isFinished { + let multiplier = ProgressFraction(completed: child.assignedCount, total: selfFraction.total) + if let additionalFraction = multiplier * child.fraction { + overallFraction = overallFraction + additionalFraction + } + } + } + return overallFraction + } + var children: [Child] + var parents: [Parent] + // Values of self's custom properties + var totalFileCount: Int + var completedFileCount: Int + var totalByteCount: UInt64 + var completedByteCount: UInt64 + var throughput: UInt64 + var estimatedTimeRemaining: Duration + // Values of self's custom additional properties + var customPropertiesInt: [MetatypeWrapper: Int] + var customPropertiesUInt64: [MetatypeWrapper: UInt64] + var customPropertiesDouble: [MetatypeWrapper: Double] + var customPropertiesString: [MetatypeWrapper: String?] + var customPropertiesURL: [MetatypeWrapper: URL?] + var customPropertiesUInt64Array: [MetatypeWrapper: UInt64] + var customPropertiesDuration: [MetatypeWrapper: Duration] +#if FOUNDATION_FRAMEWORK + var observers: [@Sendable (ObserverState) -> Void] + var interopType: InteropType? +#endif + + internal var totalCount: Int? { +#if FOUNDATION_FRAMEWORK + if let interopTotalCount = interopType?.totalCount { + return interopTotalCount + } +#endif + return selfFraction.total + } + + internal mutating func completedCountInfo() -> (Int, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopCompletedCount = interopType?.completedCount { + return (interopCompletedCount, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let completedCount = selfFraction.completed + return (completedCount, pendingUpdates) + } + + internal mutating func fractionCompletedInfo() -> (Double, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopFractionCompleted = interopType?.fractionCompleted { + return (interopFractionCompleted, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let fractionCompleted = overallFraction.fractionCompleted + return (fractionCompleted, pendingUpdates) + } + + internal var isIndeterminate: Bool { +#if FOUNDATION_FRAMEWORK + if let interopIsIndeterminate = interopType?.isIndeterminate { + return interopIsIndeterminate + } +#endif + return selfFraction.isIndeterminate + } + + internal mutating func isFinishedInfo() -> (Bool, [PendingChildUpdateInfo]?) { +#if FOUNDATION_FRAMEWORK + if let interopIsFinished = interopType?.isFinished { + return (interopIsFinished, nil) + } +#endif + // Order is important, we need to first call pendingUpdates, then call the overallFraction.fractionCompleted otherwise the overallFraction.fractionCompleted won't capture the updates. + let pendingUpdates = pendingChildrenUpdates() + let isFinished = selfFraction.isFinished + return (isFinished, pendingUpdates) + } + + internal mutating func complete(by count: Int) { + selfFraction.completed += count + +#if FOUNDATION_FRAMEWORK + switch interopType { + case .interopObservation(let observation): + observation.subprogressBridge?.manager.notifyObservers( + with: ObserverState( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + + if let _ = observation.reporterBridge { + notifyObservers( + with: ObserverState( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + } + case .interopMirror: + break + default: + break + } +#endif + } + + // MARK: Clean up dirty paths for fractional updates + // Returns information about upcoming updates to be done + internal mutating func pendingChildrenUpdates() -> [PendingChildUpdateInfo]? { + guard !children.isEmpty else { + return nil + } + + // Collect dirty children + var dirtyChildren: [(index: Int, manager: ProgressManager?, wasFinished: Bool, assignedCount: Int)] = [] + + for (idx, child) in children.enumerated() { + if child.isFractionDirty { + let wasFinished = children[idx].fraction.isFinished + let assignedCount = children[idx].assignedCount + dirtyChildren.append((index: idx, manager: child.manager, wasFinished: wasFinished, assignedCount: assignedCount)) + } + } + + guard !dirtyChildren.isEmpty else { + return nil + } + + for childInfo in dirtyChildren { + children[childInfo.index].isFractionDirty = false + } + + // Process dirty children, if child.manager is nil do not add to pending update array + var dirtyChildrenPendingUpdate: [PendingChildUpdateInfo] = [] + + for childInfo in dirtyChildren { + if let childManager = childInfo.manager { + // Add to pending update array + dirtyChildrenPendingUpdate.append(PendingChildUpdateInfo( + index: childInfo.index, + manager: childManager, + wasFinished: childInfo.wasFinished, + assignedCount: childInfo.assignedCount + )) + } else { + // Mark nil child as finished + if !childInfo.wasFinished { + children[childInfo.index].fraction.completed = children[childInfo.index].fraction.total ?? 0 + selfFraction.completed += childInfo.assignedCount + } + } + } + + // Return pending updates for processing + return dirtyChildrenPendingUpdate.isEmpty ? nil : dirtyChildrenPendingUpdate + } + + // Applies updates onto self's children array + internal mutating func updateChildrenProgressFraction(updates: [PendingChildUpdate]) { + for update in updates { + // Ensure the index is still valid + guard update.index < children.count else { continue } + + // Get the current state before actually updating + let currentWasFinished = children[update.index].fraction.isFinished + + children[update.index].fraction = update.updatedFraction + + // Only add to selfFraction if this update is transitioning from unfinished to finished + if update.updatedFraction.isFinished && !currentWasFinished { + selfFraction.completed += update.assignedCount + } + + children[update.index].isFractionDirty = false + } + } + + // MARK: Mark paths dirty + internal mutating func markChildDirty(at position: Int) -> [Parent]? { + guard position >= 0 && position < children.count else { + return nil + } + guard !children[position].isFractionDirty else { + return nil + } + children[position].isFractionDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesIntSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesUInt64Summary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesDoubleSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesStringSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesURLSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesUInt64ArraySummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: MetatypeWrapper, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].customPropertiesDurationSummary[property]?.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].totalFileCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.CompletedFileCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].completedFileCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.TotalByteCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].totalByteCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.CompletedByteCount.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].completedByteCountSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.Throughput.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].throughputSummary.isDirty = true + return parents + } + + internal mutating func markChildDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, at position: Int) -> [Parent] { + guard position >= 0 && position < children.count else { + return parents + } + children[position].estimatedTimeRemainingSummary.isDirty = true + return parents + } + + // MARK: Clean up dirty paths + internal struct IntSummaryUpdateInfo { + let currentSummary: Int + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct IntSummaryUpdate { + let index: Int + let updatedSummary: Int + } + + internal struct UInt64SummaryUpdateInfo { + let currentSummary: UInt64 + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct UInt64SummaryUpdate { + let index: Int + let updatedSummary: UInt64 + } + + internal struct DoubleSummaryUpdateInfo { + let currentSummary: Double + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Double, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct DoubleSummaryUpdate { + let index: Int + let updatedSummary: Double + } + + internal struct StringSummaryUpdateInfo { + let currentSummary: [String?] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [String?], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct StringSummaryUpdate { + let index: Int + let updatedSummary: [String?] + } + + internal struct URLSummaryUpdateInfo { + let currentSummary: [URL?] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [URL?], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct URLSummaryUpdate { + let index: Int + let updatedSummary: [URL?] + } + + internal struct UInt64ArraySummaryUpdateInfo { + let currentSummary: [UInt64] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct UInt64ArraySummaryUpdate { + let index: Int + let updatedSummary: [UInt64] + } + + internal struct DurationSummaryUpdateInfo { + let currentSummary: Duration + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] + let property: MetatypeWrapper + } + + internal struct DurationSummaryUpdate { + let index: Int + let updatedSummary: Duration + } + + internal struct FileCountUpdateInfo { + let currentSummary: Int + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] + let type: CountType + } + + internal struct FileCountUpdate { + let index: Int + let updatedSummary: Int + } + + internal struct ByteCountUpdateInfo { + let currentSummary: UInt64 + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] + let type: CountType + } + + internal struct ByteCountUpdate { + let index: Int + let updatedSummary: UInt64 + } + + internal struct ThroughputUpdateInfo { + let currentSummary: [UInt64] + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] + } + + internal struct ThroughputUpdate { + let index: Int + let updatedSummary: [UInt64] + } + + internal struct EstimatedTimeRemainingUpdateInfo { + let currentSummary: Duration + let dirtyChildren: [(index: Int, manager: ProgressManager)] + let nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] + } + + internal struct EstimatedTimeRemainingUpdate { + let index: Int + let updatedSummary: Duration + } + + internal mutating func getIntSummaryUpdateInfo(property: MetatypeWrapper) -> IntSummaryUpdateInfo { + var currentSummary: Int = property.defaultSummary + property.reduce(¤tSummary, customPropertiesInt[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return IntSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesIntSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return IntSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateIntSummary(_ updateInfo: IntSummaryUpdateInfo, _ childUpdates: [IntSummaryUpdate]) -> Int { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesIntSummary[updateInfo.property] = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getUInt64SummaryUpdateInfo(property: MetatypeWrapper) -> UInt64SummaryUpdateInfo { + var currentSummary: UInt64 = property.defaultSummary + property.reduce(¤tSummary, customPropertiesUInt64[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return UInt64SummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesUInt64Summary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return UInt64SummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateUInt64Summary(_ updateInfo: UInt64SummaryUpdateInfo, _ childUpdates: [UInt64SummaryUpdate]) -> UInt64 { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesUInt64Summary[updateInfo.property] = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getDoubleSummaryUpdateInfo(property: MetatypeWrapper) -> DoubleSummaryUpdateInfo { + var currentSummary: Double = property.defaultSummary + property.reduce(¤tSummary, customPropertiesDouble[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return DoubleSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Double, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesDoubleSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return DoubleSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateDoubleSummary(_ updateInfo: DoubleSummaryUpdateInfo, _ childUpdates: [DoubleSummaryUpdate]) -> Double { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesDoubleSummary[updateInfo.property] = PropertyStateDouble(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getStringSummaryUpdateInfo(property: MetatypeWrapper) -> StringSummaryUpdateInfo { + var currentSummary: [String?] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesString[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return StringSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [String?], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesStringSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return StringSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateStringSummary(_ updateInfo: StringSummaryUpdateInfo, _ childUpdates: [StringSummaryUpdate]) -> [String?] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesStringSummary[updateInfo.property] = PropertyStateString(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getURLSummaryUpdateInfo(property: MetatypeWrapper) -> URLSummaryUpdateInfo { + var currentSummary: [URL?] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesURL[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return URLSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [URL?], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesURLSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return URLSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateURLSummary(_ updateInfo: URLSummaryUpdateInfo, _ childUpdates: [URLSummaryUpdate]) -> [URL?] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesURLSummary[updateInfo.property] = PropertyStateURL(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getUInt64ArraySummaryUpdateInfo(property: MetatypeWrapper) -> UInt64ArraySummaryUpdateInfo { + var currentSummary: [UInt64] = property.defaultSummary + property.reduce(¤tSummary, customPropertiesUInt64Array[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return UInt64ArraySummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesUInt64ArraySummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultSummary, isAlive)) + } + } + } + + return UInt64ArraySummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateUInt64ArraySummary(_ updateInfo: UInt64ArraySummaryUpdateInfo, _ childUpdates: [UInt64ArraySummaryUpdate]) -> [UInt64] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesUInt64ArraySummary[updateInfo.property] = PropertyStateUInt64Array(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getDurationSummaryUpdateInfo(property: MetatypeWrapper) -> DurationSummaryUpdateInfo { + var currentSummary: Duration = property.defaultSummary + property.reduce(¤tSummary, customPropertiesDuration[property] ?? property.defaultValue) + + guard !children.isEmpty else { + return DurationSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + property: property + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if let childPropertyState = child.customPropertiesDurationSummary[property] { + if childPropertyState.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, childPropertyState.value, isAlive)) + } + } else { + // Property doesn't exist yet in child - need to fetch it + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child manager is deallocated, use default value + let isAlive = false + nonDirtySummaries.append((idx, property.defaultValue, isAlive)) + } + } + } + + return DurationSummaryUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + property: property + ) + } + + internal mutating func updateDurationSummary(_ updateInfo: DurationSummaryUpdateInfo, _ childUpdates: [DurationSummaryUpdate]) -> Duration { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].customPropertiesDurationSummary[updateInfo.property] = PropertyStateDuration(value: update.updatedSummary, isDirty: false) + value = updateInfo.property.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = updateInfo.property.merge(value, childSummary) + } else { + value = updateInfo.property.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getFileCountUpdateInfo(type: CountType) -> FileCountUpdateInfo { + let currentSummary: Int + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Int, isAlive: Bool)] = [] + + switch type { + case .total: + var value: Int = 0 + ProgressManager.Properties.TotalFileCount.reduce(into: &value, value: totalFileCount) + currentSummary = value + + guard !children.isEmpty else { + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.totalFileCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.totalFileCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.totalFileCountSummary.value, isAlive)) + } + } + + case .completed: + var value: Int = 0 + ProgressManager.Properties.CompletedFileCount.reduce(into: &value, value: completedFileCount) + currentSummary = value + + guard !children.isEmpty else { + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.completedFileCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.completedFileCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.completedFileCountSummary.value, isAlive)) + } + } + } + + return FileCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + type: type + ) + } + + internal mutating func updateFileCount(_ updateInfo: FileCountUpdateInfo, _ childUpdates: [FileCountUpdate]) -> Int { + var value = updateInfo.currentSummary + + switch updateInfo.type { + case .total: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].totalFileCountSummary = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.TotalFileCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.TotalFileCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.TotalFileCount.finalSummary(value, childSummary) + } + } + + case .completed: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].completedFileCountSummary = PropertyStateInt(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.CompletedFileCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.CompletedFileCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.CompletedFileCount.finalSummary(value, childSummary) + } + } + } + + return value + } + + internal mutating func getByteCountUpdateInfo(type: CountType) -> ByteCountUpdateInfo { + let currentSummary: UInt64 + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: UInt64, isAlive: Bool)] = [] + + switch type { + case .total: + var value: UInt64 = 0 + ProgressManager.Properties.TotalByteCount.reduce(into: &value, value: totalByteCount) + currentSummary = value + + guard !children.isEmpty else { + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.totalByteCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.totalByteCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.totalByteCountSummary.value, isAlive)) + } + } + + case .completed: + var value: UInt64 = 0 + ProgressManager.Properties.CompletedByteCount.reduce(into: &value, value: completedByteCount) + currentSummary = value + + guard !children.isEmpty else { + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [], + type: type + ) + } + + for (idx, child) in children.enumerated() { + if child.completedByteCountSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.completedByteCountSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.completedByteCountSummary.value, isAlive)) + } + } + } + + return ByteCountUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries, + type: type + ) + } + + internal mutating func updateByteCount(_ updateInfo: ByteCountUpdateInfo, _ childUpdates: [ByteCountUpdate]) -> UInt64 { + var value = updateInfo.currentSummary + + switch updateInfo.type { + case .total: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].totalByteCountSummary = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.TotalByteCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.TotalByteCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.TotalByteCount.finalSummary(value, childSummary) + } + } + + case .completed: + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].completedByteCountSummary = PropertyStateUInt64(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.CompletedByteCount.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.CompletedByteCount.merge(value, childSummary) + } else { + value = ProgressManager.Properties.CompletedByteCount.finalSummary(value, childSummary) + } + } + } + + return value + } + + internal mutating func getThroughputUpdateInfo() -> ThroughputUpdateInfo { + var currentSummary = ProgressManager.Properties.Throughput.defaultSummary + ProgressManager.Properties.Throughput.reduce(into: ¤tSummary, value: throughput) + + guard !children.isEmpty else { + return ThroughputUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [] + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: [UInt64], isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if child.throughputSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.throughputSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.throughputSummary.value, isAlive)) + } + } + + return ThroughputUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries + ) + } + + internal mutating func updatedThroughput(_ updateInfo: ThroughputUpdateInfo, _ childUpdates: [ThroughputUpdate]) -> [UInt64] { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].throughputSummary = PropertyStateUInt64Array(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.Throughput.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.Throughput.merge(value, childSummary) + } else { + value = ProgressManager.Properties.Throughput.finalSummary(value, childSummary) + } + } + + return value + } + + internal mutating func getEstimatedTimeRemainingUpdateInfo() -> EstimatedTimeRemainingUpdateInfo { + var currentSummary: Duration = Duration.seconds(0) + ProgressManager.Properties.EstimatedTimeRemaining.reduce(into: ¤tSummary, value: estimatedTimeRemaining) + + guard !children.isEmpty else { + return EstimatedTimeRemainingUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: [], + nonDirtySummaries: [] + ) + } + + var dirtyChildren: [(index: Int, manager: ProgressManager)] = [] + var nonDirtySummaries: [(index: Int, summary: Duration, isAlive: Bool)] = [] + + for (idx, child) in children.enumerated() { + if child.estimatedTimeRemainingSummary.isDirty { + if let child = child.manager { + dirtyChildren.append((idx, child)) + } else { + // Child is dirty but manager is deallocated - use last known value + let isAlive = false + nonDirtySummaries.append((idx, child.estimatedTimeRemainingSummary.value, isAlive)) + } + } else { + let isAlive = child.manager != nil + nonDirtySummaries.append((idx, child.estimatedTimeRemainingSummary.value, isAlive)) + } + } + + return EstimatedTimeRemainingUpdateInfo( + currentSummary: currentSummary, + dirtyChildren: dirtyChildren, + nonDirtySummaries: nonDirtySummaries + ) + } + + internal mutating func updateEstimatedTimeRemaining(_ updateInfo: EstimatedTimeRemainingUpdateInfo, _ childUpdates: [EstimatedTimeRemainingUpdate]) -> Duration { + var value = updateInfo.currentSummary + + // Apply updates from children that were dirty + for update in childUpdates { + children[update.index].estimatedTimeRemainingSummary = PropertyStateDuration(value: update.updatedSummary, isDirty: false) + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, update.updatedSummary) + } + + // Apply values from non-dirty children + for (_, childSummary, isAlive) in updateInfo.nonDirtySummaries { + if isAlive { + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, childSummary) + } else { + value = ProgressManager.Properties.EstimatedTimeRemaining.finalSummary(value, childSummary) + } + } + + return value + } + + struct FinalSummary { + var totalFileCountSummary: Int + var completedFileCountSummary: Int + var totalByteCountSummary: UInt64 + var completedByteCountSummary: UInt64 + var throughputSummary: [UInt64] + var estimatedTimeRemainingSummary: Duration + var customPropertiesIntSummary: [MetatypeWrapper: Int] + var customPropertiesUInt64Summary: [MetatypeWrapper: UInt64] + var customPropertiesDoubleSummary: [MetatypeWrapper: Double] + var customPropertiesStringSummary: [MetatypeWrapper: [String?]] + var customPropertiesURLSummary: [MetatypeWrapper: [URL?]] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: [UInt64]] + var customPropertiesDurationSummary: [MetatypeWrapper: Duration] + } + + func customPropertiesCleanup() -> (FinalSummary, [Parent]) { + // Set up default summaries + var totalFileCount = Properties.TotalFileCount.defaultSummary + var completedFileCount = Properties.CompletedFileCount.defaultSummary + var totalByteCount = Properties.TotalByteCount.defaultSummary + var completedByteCount = Properties.CompletedByteCount.defaultSummary + var throughput = Properties.Throughput.defaultSummary + var estimatedTimeRemaining = Properties.EstimatedTimeRemaining.defaultSummary + var customPropertiesIntSummary: [MetatypeWrapper: Int] = [:] + var customPropertiesUInt64Summary: [MetatypeWrapper: UInt64] = [:] + var customPropertiesDoubleSummary: [MetatypeWrapper: Double] = [:] + var customPropertiesStringSummary: [MetatypeWrapper: [String?]] = [:] + var customPropertiesURLSummary: [MetatypeWrapper: [URL?]] = [:] + var customPropertiesUInt64ArraySummary: [MetatypeWrapper: [UInt64]] = [:] + var customPropertiesDurationSummary: [MetatypeWrapper: Duration] = [:] + + // Include self's custom properties values + Properties.TotalFileCount.reduce(into: &totalFileCount, value: self.totalFileCount) + Properties.CompletedFileCount.reduce(into: &completedFileCount, value: self.completedFileCount) + Properties.TotalByteCount.reduce(into: &totalByteCount, value: self.totalByteCount) + Properties.CompletedByteCount.reduce(into: &completedByteCount, value: self.completedByteCount) + Properties.Throughput.reduce(into: &throughput, value: self.throughput) + Properties.EstimatedTimeRemaining.reduce(into: &estimatedTimeRemaining, value: self.estimatedTimeRemaining) + + // MARK: Custom Properties (Int, Int) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesInt { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesIntSummary[key] = summary + } + + // MARK: Custom Properties (UInt64, UInt64) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesUInt64 { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesUInt64Summary[key] = summary + } + + // MARK: Custom Properties (UInt64, [UInt64]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesUInt64Array { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesUInt64ArraySummary[key] = summary + } + + // MARK: Custom Properties (Double, Double) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesDouble { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesDoubleSummary[key] = summary + } + + // MARK: Custom Properties (String?, [String?]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesString { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesStringSummary[key] = summary + } + + // MARK: Custom Properties (URL?, [URL?]) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesURL { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesURLSummary[key] = summary + } + + // MARK: Custom Properties (Duration, Duration) + // Aggregate information using self's custom property keys + for (key, value) in customPropertiesDuration { + // Set up overall summary + var summary = key.defaultSummary + + // Include self's value into summary + key.reduce(&summary, value) + + // Save summary to dictionary + customPropertiesDurationSummary[key] = summary + } + + // Include child's custom properties summaries, we need to take into account the fact that some of the children's custom properties may not be in self, so we need to check that too. As for the ones that are in self, we need to call finalSummary. + for child in children { + totalFileCount = Properties.TotalFileCount.finalSummary(totalFileCount, child.totalFileCountSummary.value) + completedFileCount = Properties.CompletedFileCount.finalSummary(completedFileCount, child.completedFileCountSummary.value) + totalByteCount = Properties.TotalByteCount.finalSummary(totalByteCount, child.totalByteCountSummary.value) + completedByteCount = Properties.CompletedByteCount.finalSummary(completedByteCount, child.completedByteCountSummary.value) + throughput = Properties.Throughput.finalSummary(throughput, child.throughputSummary.value) + estimatedTimeRemaining = Properties.EstimatedTimeRemaining.finalSummary(estimatedTimeRemaining, child.estimatedTimeRemainingSummary.value) + + for (key, _) in customPropertiesInt { + customPropertiesIntSummary[key] = key.finalSummary(customPropertiesIntSummary[key] ?? key.defaultSummary, child.customPropertiesIntSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesIntSummary { + if !customPropertiesInt.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesIntSummary[key] = summary + } + } + + + for (key, _) in customPropertiesUInt64 { + customPropertiesUInt64Summary[key] = key.finalSummary(customPropertiesUInt64Summary[key] ?? key.defaultSummary, child.customPropertiesUInt64Summary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesUInt64Summary { + if !customPropertiesUInt64.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesUInt64Summary[key] = summary + } + } + + + for (key, _) in customPropertiesUInt64Array { + customPropertiesUInt64ArraySummary[key] = key.finalSummary(customPropertiesUInt64ArraySummary[key] ?? key.defaultSummary, child.customPropertiesUInt64ArraySummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesUInt64ArraySummary { + if !customPropertiesUInt64Array.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesUInt64ArraySummary[key] = summary + } + } + + for (key, _) in customPropertiesDouble { + customPropertiesDoubleSummary[key] = key.finalSummary(customPropertiesDoubleSummary[key] ?? key.defaultSummary, child.customPropertiesDoubleSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesDoubleSummary { + if !customPropertiesDouble.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesDoubleSummary[key] = summary + } + } + + for (key, _) in customPropertiesString { + customPropertiesStringSummary[key] = key.finalSummary(customPropertiesStringSummary[key] ?? key.defaultSummary, child.customPropertiesStringSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesStringSummary { + if !customPropertiesString.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesStringSummary[key] = summary + } + } + + for (key, _) in customPropertiesURL { + customPropertiesURLSummary[key] = key.finalSummary(customPropertiesURLSummary[key] ?? key.defaultSummary, child.customPropertiesURLSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesURLSummary { + if !customPropertiesURL.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesURLSummary[key] = summary + } + } + + for (key, _) in customPropertiesDuration { + customPropertiesDurationSummary[key] = key.finalSummary(customPropertiesDurationSummary[key] ?? key.defaultSummary, child.customPropertiesDurationSummary[key]?.value ?? key.defaultSummary) + } + + // Aggregate information using child's custom property keys that may be absent from self's custom property keys + for (key, value) in child.customPropertiesDurationSummary { + if !customPropertiesDuration.keys.contains(key) { + // Set up default summary + var summary = key.defaultSummary + // Include child's value + summary = key.finalSummary(summary, value.value) + // Save summary value to dictionary + customPropertiesDurationSummary[key] = summary + } + } + } + + return (FinalSummary(totalFileCountSummary: totalFileCount, + completedFileCountSummary: completedFileCount, + totalByteCountSummary: totalByteCount, + completedByteCountSummary: completedByteCount, + throughputSummary: throughput, + estimatedTimeRemainingSummary: estimatedTimeRemaining, + customPropertiesIntSummary: customPropertiesIntSummary, + customPropertiesUInt64Summary: customPropertiesUInt64Summary, + customPropertiesDoubleSummary: customPropertiesDoubleSummary, + customPropertiesStringSummary: customPropertiesStringSummary, + customPropertiesURLSummary: customPropertiesURLSummary, + customPropertiesUInt64ArraySummary: customPropertiesUInt64ArraySummary, + customPropertiesDurationSummary: customPropertiesDurationSummary + ), + parents) + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift new file mode 100644 index 000000000..8f0e62994 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -0,0 +1,567 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation +#if canImport(Synchronization) +internal import Synchronization +#endif + +#if canImport(CollectionsInternal) +internal import CollectionsInternal +#elseif canImport(OrderedCollections) +internal import OrderedCollections +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +/// An object that conveys ongoing progress to the user for a specified task. +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup +@Observable public final class ProgressManager: Sendable { + + internal let state: Mutex + // These are "fake" keypaths used for registering observations of values and summaries of custom properties declared by developers. + internal let customPropertiesInt: Void + internal let customPropertiesUInt64: Void + internal let customPropertiesDouble: Void + internal let customPropertiesString: Void + internal let customPropertiesURL: Void + internal let customPropertiesUInt64Array: Void + internal let customPropertiesDuration: Void + internal let customPropertiesIntSummary: Void + internal let customPropertiesUInt64Summary: Void + internal let customPropertiesDoubleSummary: Void + internal let customPropertiesStringSummary: Void + internal let customPropertiesURLSummary: Void + internal let customPropertiesUInt64ArraySummary: Void + internal let customPropertiesDurationSummary: Void + // These are "fake" keypaths used for registering observations of summaries of pre-declared custom properties. + internal let totalFileCountSummary: Void + internal let completedFileCountSummary: Void + internal let totalByteCountSummary: Void + internal let completedByteCountSummary: Void + internal let throughputSummary: Void + internal let estimatedTimeRemainingSummary: Void + + /// The total units of work. + public var totalCount: Int? { + self.access(keyPath: \.totalCount) + return state.withLock { state in + state.totalCount + } + } + + /// The completed units of work. + public var completedCount: Int { + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, completedCount, pendingUpdates) = state.withLock { state in + let (completedCount, pendingUpdates) = state.completedCountInfo() + return (state.children.compactMap { $0.manager }, completedCount, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.completedCount) + } + return completedCount + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedCompletedCount = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.selfFraction.completed + } + + for child in children { + child.access(keyPath: \.completedCount) + } + + return updatedCompletedCount + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0.0. + public var fractionCompleted: Double { + self.access(keyPath: \.totalCount) + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, fractionCompleted, pendingUpdates) = state.withLock { state in + let (fractionCompleted, pendingUpdates) = state.fractionCompletedInfo() + return (state.children.compactMap { $0.manager }, fractionCompleted, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + return fractionCompleted + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedFractionCompleted = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.overallFraction.fractionCompleted + } + + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + + return updatedFractionCompleted + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + self.access(keyPath: \.totalCount) + return state.withLock { state in + state.isIndeterminate + } + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + self.access(keyPath: \.totalCount) + self.access(keyPath: \.completedCount) + + // Get information about dirty children (Acquire and release self's lock) + let (children, isFinished, pendingUpdates) = state.withLock { state in + let (isFinished, pendingUpdates) = state.isFinishedInfo() + return (state.children.compactMap { $0.manager }, isFinished, pendingUpdates) + } + + guard let updates = pendingUpdates else { + for child in children { + child.access(keyPath: \.totalCount) + child.access(keyPath: \.completedCount) + } + return isFinished + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + let updatedIsFinished = state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.selfFraction.isFinished + } + + for child in children { + child.access(keyPath: \.completedCount) + } + + return updatedIsFinished + } + + /// A `ProgressReporter` instance, used for providing read-only observation of progress updates or composing into other `ProgressManager`s. + public var reporter: ProgressReporter { + return .init(manager: self) + } + +#if FOUNDATION_FRAMEWORK + internal init(total: Int?, completed: Int?, subprogressBridge: SubprogressBridge?) { + let state = State( + selfFraction: ProgressFraction(completed: completed ?? 0, total: total), + children: [], + parents: [], + totalFileCount: ProgressManager.Properties.TotalFileCount.defaultValue, + completedFileCount: ProgressManager.Properties.CompletedFileCount.defaultValue, + totalByteCount: ProgressManager.Properties.TotalByteCount.defaultValue, + completedByteCount: ProgressManager.Properties.CompletedByteCount.defaultValue, + throughput: ProgressManager.Properties.Throughput.defaultValue, + estimatedTimeRemaining: ProgressManager.Properties.EstimatedTimeRemaining.defaultValue, + customPropertiesInt: [:], + customPropertiesUInt64: [:], + customPropertiesDouble: [:], + customPropertiesString: [:], + customPropertiesURL: [:], + customPropertiesUInt64Array: [:], + customPropertiesDuration: [:], + observers: [], + interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge)) + ) + self.state = Mutex(state) + } +#else + internal init(total: Int?, completed: Int?) { + let state = State( + selfFraction: ProgressFraction(completed: completed ?? 0, total: total), + children: [], + parents: [], + totalFileCount: ProgressManager.Properties.TotalFileCount.defaultValue, + completedFileCount: ProgressManager.Properties.CompletedFileCount.defaultValue, + totalByteCount: ProgressManager.Properties.TotalByteCount.defaultValue, + completedByteCount: ProgressManager.Properties.CompletedByteCount.defaultValue, + throughput: ProgressManager.Properties.Throughput.defaultValue, + estimatedTimeRemaining: ProgressManager.Properties.EstimatedTimeRemaining.defaultValue, + customPropertiesInt: [:], + customPropertiesUInt64: [:], + customPropertiesDouble: [:], + customPropertiesString: [:], + customPropertiesURL: [:], + customPropertiesUInt64Array: [:], + customPropertiesDuration: [:] + ) + self.state = Mutex(state) + } +#endif + + /// Initializes `self` with `totalCount`. + /// + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) { + #if FOUNDATION_FRAMEWORK + self.init( + total: totalCount, + completed: nil, + subprogressBridge: nil + ) + #else + self.init( + total: totalCount, + completed: nil, + ) + #endif + } + + /// Returns a `Subprogress` representing a portion of `self` which can be passed to any method that reports progress. + /// + /// If the `Subprogress` is not converted into a `ProgressManager` (for example, due to an error or early return), + /// then the assigned count is marked as completed in the parent `ProgressManager`. + /// + /// - Parameter count: The portion of `totalCount` to be delegated to the `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParentTotal: Int) -> Subprogress { + precondition(portionOfParentTotal > 0, "Giving out zero units is not a valid operation.") + let subprogress = Subprogress(parent: self, assignedCount: portionOfParentTotal) + return subprogress + } + + /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. + /// + /// If a cycle is detected, this will cause a crash at runtime. + /// + /// - Parameters: + /// - count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + /// - reporter: A `ProgressReporter` instance. + public func assign(count: Int, to reporter: ProgressReporter) { + precondition(isCycle(reporter: reporter) == false, "Creating a cycle is not allowed.") + + let actualManager = reporter.manager + + let position = self.addChild(childManager: actualManager, assignedCount: count, childFraction: actualManager.getProgressFraction()) + actualManager.addParent(parentManager: self, positionInParent: position) + } + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + self.withMutation(keyPath: \.completedCount) { + let parents: [Parent]? = state.withLock { state in + guard state.selfFraction.completed != (state.selfFraction.completed + count) else { + return nil + } + + state.complete(by: count) + + return state.parents + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + } + + public func setCounts(_ counts: (_ completed: inout Int, _ total: inout Int?) -> Void) { + self.withMutation(keyPath: \.completedCount) { + self.withMutation(keyPath: \.totalCount) { + let parents: [Parent]? = state.withLock { state in + var completed = state.selfFraction.completed + var total = state.selfFraction.total + + counts(&completed, &total) + + guard state.selfFraction.completed != completed || state.selfFraction.total != total else { + return nil + } + + state.selfFraction.completed = completed + state.selfFraction.total = total + + return state.parents + } + + if let parents = parents { + markSelfDirty(parents: parents) + } + } + } + } + + //MARK: Observation Methods + internal func willSet(keyPath: KeyPath) { + _$observationRegistrar.willSet(self, keyPath: keyPath) + } + + internal func didSet(keyPath: KeyPath) { + _$observationRegistrar.didSet(self, keyPath: keyPath) + } + + //MARK: Fractional Properties Methods + internal func getProgressFraction() -> ProgressFraction { + return state.withLock { state in + return state.selfFraction + } + } + + //MARK: Fractional Calculation methods + internal func markSelfDirty() { + let parents = state.withLock { state in + return state.parents + } + markSelfDirty(parents: parents) + } + + internal func markSelfDirty(parents: [Parent]) { + if parents.count > 0 { + for parent in parents { + parent.manager.markChildDirty(at: parent.positionInParent) + } + } + } + + private func markChildDirty(at position: Int) { + let parents: [Parent]? = state.withLock { state in + state.markChildDirty(at: position) + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + + internal func updatedProgressFraction() -> ProgressFraction { + // Get information about dirty children (Acquire and release self's lock) + let pendingUpdates = state.withLock { state in + state.pendingChildrenUpdates() + } + + guard let updates = pendingUpdates else { + // No pending updates, just return the overall fraction + return state.withLock { state in + state.overallFraction + } + } + + // Get updated information for each dirty child (Acquire and release each child's lock) + var childrenUpdates: [PendingChildUpdate] = [] + + for update in updates { + let updatedFraction = update.manager.updatedProgressFraction() + childrenUpdates.append(PendingChildUpdate( + index: update.index, + updatedFraction: updatedFraction, + assignedCount: update.assignedCount + )) + } + + // Apply updated information of dirty child in self's children array (Acquire and release self's lock) + return state.withLock { state in + state.updateChildrenProgressFraction(updates: childrenUpdates) + return state.overallFraction + } + } + + + //MARK: Parent - Child Relationship Methods + internal func addChild(childManager: ProgressManager, assignedCount: Int, childFraction: ProgressFraction) -> Int { + self.withMutation(keyPath: \.completedCount) { + let (index, parents) = state.withLock { state in + let child = Child(manager: childManager, + assignedCount: assignedCount, + fraction: childFraction, + isFractionDirty: true, + totalFileCountSummary: PropertyStateInt(value: ProgressManager.Properties.TotalFileCount.defaultSummary, isDirty: false), + completedFileCountSummary: PropertyStateInt(value: ProgressManager.Properties.CompletedFileCount.defaultSummary, isDirty: false), + totalByteCountSummary: PropertyStateUInt64(value: ProgressManager.Properties.TotalByteCount.defaultSummary, isDirty: false), + completedByteCountSummary: PropertyStateUInt64(value: ProgressManager.Properties.CompletedByteCount.defaultSummary, isDirty: false), + throughputSummary: PropertyStateUInt64Array(value: ProgressManager.Properties.Throughput.defaultSummary, isDirty: false), + estimatedTimeRemainingSummary: PropertyStateDuration(value: ProgressManager.Properties.EstimatedTimeRemaining.defaultSummary, isDirty: false), + customPropertiesIntSummary: [:], + customPropertiesUInt64Summary: [:], + customPropertiesDoubleSummary: [:], + customPropertiesStringSummary: [:], + customPropertiesURLSummary: [:], + customPropertiesUInt64ArraySummary: [:], + customPropertiesDurationSummary: [:]) + state.children.append(child) + return (state.children.count - 1, state.parents) + } + // Mark dirty all the way up to the root so that if the branch was marked not dirty right before this it will be marked dirty again (for optimization to work) + markSelfDirty(parents: parents) + return index + } + } + + internal func addParent(parentManager: ProgressManager, positionInParent: Int) { + state.withLock { state in + let parent = Parent(manager: parentManager, positionInParent: positionInParent) + state.parents.append(parent) + } + } + + // MARK: Cycle Detection Methods + internal func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + if reporter.manager === self { + return true + } + let updatedVisited = visited.union([self]) + let parents = state.withLock { state in + return state.parents + } + for parent in parents { + guard !updatedVisited.contains(parent.manager) else { + continue + } + if parent.manager.isCycle(reporter: reporter, visited: updatedVisited) { + return true + } + } + return false + } + + internal func isCycleInterop(reporter: ProgressReporter, visited: Set = []) -> Bool { + let parents = state.withLock { state in + return state.parents + } + for parent in parents { + guard !visited.contains(parent.manager) else { + continue + } + if parent.manager.isCycle(reporter: reporter, visited: visited) { + return true + } + } + return false + } + + deinit { + // Custom Properties directly updates parents' entries so it should not be marked dirty. + let (finalSummary, parents) = state.withLock { state in + state.customPropertiesCleanup() + } + + for parent in parents { + parent.manager.setChildDeclaredAdditionalProperties( + at: parent.positionInParent, + totalFileCount: finalSummary.totalFileCountSummary, + completedFileCount: finalSummary.completedFileCountSummary, + totalByteCount: finalSummary.totalByteCountSummary, + completedByteCount: finalSummary.completedByteCountSummary, + throughput: finalSummary.throughputSummary, + estimatedTimeRemaining: finalSummary.estimatedTimeRemainingSummary, + propertiesInt: finalSummary.customPropertiesIntSummary, + propertiesUInt64: finalSummary.customPropertiesUInt64Summary, + propertiesDouble: finalSummary.customPropertiesDoubleSummary, + propertiesString: finalSummary.customPropertiesStringSummary, + propertiesURL: finalSummary.customPropertiesURLSummary, + propertiesUInt64Array: finalSummary.customPropertiesUInt64ArraySummary, + propertiesDuration: finalSummary.customPropertiesDurationSummary + ) + } + + // Fractional property does not update parents' entries so it should be marked dirty. If fraction is not finished when deinit, mark the path dirty so that parents have an opportunity to check and complete the portion that is unfinished. The parent will later complete the portion when trying to clear the dirty bit in state.updateChildrenProgressFraction(). + if !isFinished { + markSelfDirty(parents: parents) + } + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager: Hashable, Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + /// Returns `true` if pointer of `lhs` is equal to pointer of `rhs`. + public static func ==(lhs: ProgressManager, rhs: ProgressManager) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +@available(FoundationPreview 6.4, *) +extension ProgressManager: CustomStringConvertible, CustomDebugStringConvertible { + /// A description. + public var description: String { + return """ + Class Name: ProgressManager + Object Identifier: \(ObjectIdentifier(self)) + totalCount: \(String(describing: totalCount)) + completedCount: \(completedCount) + fractionCompleted: \(fractionCompleted) + isIndeterminate: \(isIndeterminate) + isFinished: \(isFinished) + totalFileCount: \(summary(of: ProgressManager.Properties.TotalFileCount.self)) + completedFileCount: \(summary(of: ProgressManager.Properties.CompletedFileCount.self)) + totalByteCount: \(summary(of: ProgressManager.Properties.TotalByteCount.self)) + completedByteCount: \(summary(of: ProgressManager.Properties.CompletedByteCount.self)) + throughput: \(summary(of: ProgressManager.Properties.Throughput.self)) + estimatedTimeRemaining: \(summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self)) + """ + } + + /// A debug description. + public var debugDescription: String { + return self.description + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift new file mode 100644 index 000000000..f343297ea --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -0,0 +1,273 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Observation + +/// ProgressReporter is a wrapper for ProgressManager that carries information about ProgressManager. +/// +/// It is read-only and can be added as a child of another ProgressManager. +@available(FoundationPreview 6.4, *) +@dynamicMemberLookup +@Observable public final class ProgressReporter: Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { + + public typealias Property = ProgressManager.Property + + /// The total units of work. + public var totalCount: Int? { + manager.totalCount + } + + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { + manager.completedCount + } + + /// The proportion of work completed. + /// This takes into account the fraction completed in its children instances if children are present. + /// If `self` is indeterminate, the value will be 0. + public var fractionCompleted: Double { + manager.fractionCompleted + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + manager.isIndeterminate + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + manager.isFinished + } + + /// A textual representation of the progress reporter. + /// + /// This property provides a comprehensive description including the class name, object identifier, + /// underlying progress manager details, and various progress metrics and properties. + public var description: String { + return """ + Class Name: ProgressReporter + Object Identifier: \(ObjectIdentifier(self)) + progressManager: \(manager) + totalCount: \(String(describing: totalCount)) + completedCount: \(completedCount) + fractionCompleted: \(fractionCompleted) + isIndeterminate: \(isIndeterminate) + isFinished: \(isFinished) + totalFileCount: \(summary(of: ProgressManager.Properties.TotalFileCount.self)) + completedFileCount: \(summary(of: ProgressManager.Properties.CompletedFileCount.self)) + totalByteCount: \(summary(of: ProgressManager.Properties.TotalByteCount.self)) + completedByteCount: \(summary(of: ProgressManager.Properties.CompletedByteCount.self)) + throughput: \(summary(of: ProgressManager.Properties.Throughput.self)) + estimatedTimeRemaining: \(summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self)) + """ + } + + /// A textual representation of the progress reporter suitable for debugging. + /// + /// This property returns the same value as `description`, providing detailed information + /// about the progress reporter's state for debugging purposes. + public var debugDescription: String { + return self.description + } + + /// Returns a summary for the specified integer property across the progress subtree. + /// + /// This method aggregates the values of a custom integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the integer property to summarize. Must be a property + /// where both the value and summary types are `Int`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> Int where P.Value == Int, P.Summary == Int { + manager.summary(of: property) + } + + /// Returns a summary for the specified unsigned integer property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the unsigned property to summarize. Must be a property + /// where both the value and summary types are `UInt64`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + manager.summary(of: property) + } + + /// Returns a summary for the specified double property across the progress subtree. + /// + /// This method aggregates the values of a custom double property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the double property to summarize. Must be a property + /// where both the value and summary types are `Double`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> Double where P.Value == Double, P.Summary == Double { + manager.summary(of: property) + } + + /// Returns a summary for the specified string property across the progress subtree. + /// + /// This method aggregates the values of a custom string property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the string property to summarize. Must be a property + /// where both the value and summary types are `String`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> [String?] where P.Value == String?, P.Summary == [String?] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified URL property across the progress subtree. + /// + /// This method aggregates the values of a custom URL property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the URL property to summarize. Must be a property + /// where both the value and summary types are `URL?` and `[URL?]` respectively. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> [URL?] where P.Value == URL?, P.Summary == [URL?] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified unsigned integer array property across the progress subtree. + /// + /// This method aggregates the values of a custom unsigned integer property from the underlying progress manager + /// and all its children, returning a consolidated summary value as an array. + /// + /// - Parameter property: The type of the unsigned integer property to summarize. Must be a property + /// where the value type is `UInt64` and the summary type is `[UInt64]`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> [UInt64] where P.Value == UInt64, P.Summary == [UInt64] { + return manager.summary(of: property) + } + + /// Returns a summary for the specified duration property across the progress subtree. + /// + /// This method aggregates the values of a custom duration property from the underlying progress manager + /// and all its children, returning a consolidated summary value. + /// + /// - Parameter property: The type of the duration property to summarize. Must be a property + /// where both the value and summary types are `Duration`. + /// - Returns: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> Duration where P.Value == Duration, P.Summary == Duration { + return manager.summary(of: property) + } + + /// Gets or sets custom integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Int`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom integer property type. + public subscript(dynamicMember key: KeyPath) -> Int where P.Value == Int, P.Summary == Int { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `UInt64`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == UInt64 { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom double properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `Double`. If the property has not been set, the getter returns the + /// property's default value. + /// + /// - Parameter key: A key path to the custom double property type. + public subscript(dynamicMember key: KeyPath) -> P.Value where P.Value == Double, P.Summary == Double { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `String?` and the summary type is `[String?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom string property type. + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom URL properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `URL?` and the summary type is `[URL?]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom URL property type. + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom unsigned integer properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `UInt64` and the summary type is `[UInt64]`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom unsigned integer property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 where P.Value == UInt64, P.Summary == [UInt64] { + get { + manager[dynamicMember: key] + } + } + + /// Gets or sets custom duration properties. + /// + /// This subscript provides read-write access to custom progress properties where the value + /// type is `Duration` and the summary type is `Duration`. If the property has not been set, + /// the getter returns the property's default value. + /// + /// - Parameter key: A key path to the custom duration property type. + public subscript(dynamicMember key: KeyPath) -> Duration where P.Value == Duration, P.Summary == Duration { + get { + manager[dynamicMember: key] + } + } + + internal let manager: ProgressManager + + internal init(manager: ProgressManager) { + self.manager = manager + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + public static func == (lhs: ProgressReporter, rhs: ProgressReporter) -> Bool { + ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift new file mode 100644 index 000000000..2e960d4bd --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift @@ -0,0 +1,83 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +/// Subprogress is used to establish parent-child relationship between two instances of `ProgressManager`. +/// +/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressManager. +/// A child ProgressManager is then returned by calling `start(totalCount:)` on a Subprogress. +@available(FoundationPreview 6.4, *) +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressManager + internal var assignedCount: Int + internal var isInitializedToProgressReporter: Bool +#if FOUNDATION_FRAMEWORK + internal var subprogressBridge: SubprogressBridge? +#endif + +#if FOUNDATION_FRAMEWORK + internal init(parent: ProgressManager, assignedCount: Int, subprogressBridge: SubprogressBridge? = nil) { + self.parent = parent + self.assignedCount = assignedCount + self.isInitializedToProgressReporter = false + self.subprogressBridge = subprogressBridge + } +#else + internal init(parent: ProgressManager, assignedCount: Int) { + self.parent = parent + self.assignedCount = assignedCount + self.isInitializedToProgressReporter = false + } +#endif + + /// Instantiates a ProgressManager which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressManager` instance. + /// - Returns: A `ProgressManager` instance. + public consuming func start(totalCount: Int?) -> ProgressManager { + isInitializedToProgressReporter = true + +#if FOUNDATION_FRAMEWORK + let childManager = ProgressManager( + total: totalCount, + completed: nil, + subprogressBridge: subprogressBridge + ) + + guard subprogressBridge == nil else { + subprogressBridge?.manager.setInteropChild(interopMirror: childManager) + return childManager + } +#else + let childManager = ProgressManager( + total: totalCount, + completed: nil + ) +#endif + + let position = parent.addChild( + childManager: childManager, + assignedCount: assignedCount, + childFraction: childManager.getProgressFraction() + ) + childManager.addParent( + parentManager: parent, + positionInParent: position + ) + + return childManager + } + + deinit { + if !self.isInitializedToProgressReporter { + parent.complete(count: assignedCount) + } + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift new file mode 100644 index 000000000..49728d456 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressFractionTests.swift @@ -0,0 +1,167 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +@Suite("Progress Fraction", .tags(.progressManager)) struct ProgressFractionTests { + @Test func equal() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 100, total: 200) + + #expect(f1 == f2) + + let f3 = ProgressFraction(completed: 3, total: 10) + #expect(f1 != f3) + + let f4 = ProgressFraction(completed: 5, total: 10) + #expect(f1 == f4) + } + + @Test func addSame() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = f1 + f2 + #expect(r.completed == 8) + #expect(r.total == 10) + } + + @Test func addDifferent() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed : 300, total: 1000) + + let r = f1 + f2 + #expect(r.completed == 800) + #expect(r.total == 1000) + } + + @Test func subtract() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = f1 - f2 + #expect(r.completed == 2) + #expect(r.total == 10) + } + + @Test func multiply() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 1, total: 2) + + let r = f1 * f2 + #expect(r?.completed == 5) + #expect(r?.total == 20) + } + + @Test func simplify() { + let f1 = ProgressFraction(completed: 5, total: 10) + let f2 = ProgressFraction(completed: 3, total: 10) + + let r = (f1 + f2).simplified() + + #expect(r?.completed == 4) + #expect(r?.total == 5) + } + + @Test func overflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + + var f1 = ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + ProgressFraction(completed: 1, total: d) + } + + let fractionResult = f1.fractionCompleted + var expectedResult = 1.0 / 3.0 + for d in denominators { + expectedResult = expectedResult + 1.0 / Double(d) + } + #expect(abs(fractionResult - expectedResult) < 0.00001) + } + + @Test func addOverflow() { + // These prime numbers are problematic for overflowing + let denominators : [Int] = [5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 69] + var f1 = ProgressFraction(completed: 1, total: 3) + for d in denominators { + f1 = f1 + ProgressFraction(completed: 1, total: d) + } + + // f1 should be in overflow + #expect(f1.overflowed) + + let f2 = ProgressFraction(completed: 1, total: 4) + f1 + + // f2 should also be in overflow + #expect(f2.overflowed) + + // And it should have completed value of about 1.0/4.0 + f1.fractionCompleted + let expected = (1.0 / 4.0) + f1.fractionCompleted + + #expect(abs(expected - f2.fractionCompleted) < 0.00001) + } + +#if _pointerBitWidth(_64) // These tests assumes Int is Int64 + @Test func addAndSubtractOverflow() { + let f1 = ProgressFraction(completed: 48, total: 60) + let f2 = ProgressFraction(completed: 5880, total: 7200) + let f3 = ProgressFraction(completed: 7048893638467736640, total: 8811117048084670800) + + let result1 = (f3 - f1) + f2 + #expect(result1.completed > 0) + + let result2 = (f3 - f2) + f1 + #expect(result2.completed < 60) + } + + @Test func subtractOverflow() { + let f1 = ProgressFraction(completed: 9855, total: 225066) + let f2 = ProgressFraction(completed: 14985363210613129, total: 56427817205760000) + + let result = f2 - f1 + #expect(abs(Double(result.completed) / Double(result.total!) - 0.2217) < 0.01) + } + + @Test func multiplyOverflow() { + let f1 = ProgressFraction(completed: 4294967279, total: 4294967291) + let f2 = ProgressFraction(completed: 4294967279, total: 4294967291) + + let result = f1 * f2 + #expect(abs(Double(result!.completed) / Double(result!.total!) - 1.0) < 0.01) + } +#endif + + @Test func fractionFromDouble() { + let d = 4.25 // exactly representable in binary + let f1 = ProgressFraction(double: d) + + let simplified = f1.simplified() + #expect(simplified?.completed == 17) + #expect(simplified?.total == 4) + } + + @Test func unnecessaryOverflow() { + // just because a fraction has a large denominator doesn't mean it needs to overflow + let f1 = ProgressFraction(completed: (Int.max - 1) / 2, total: Int.max - 1) + let f2 = ProgressFraction(completed: 1, total: 16) + + let r = f1 + f2 + #expect(!r.overflowed) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift new file mode 100644 index 000000000..87cd6efad --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerInteropTests.swift @@ -0,0 +1,332 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation + +/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressManager +@Suite("Progress Manager Interop", .tags(.progressManager)) struct ProgressManagerInteropTests { + func doSomethingWithProgress() async -> Progress { + let p = Progress(totalUnitCount: 2) + return p + } + + func doSomething(subprogress: consuming Subprogress?) async { + let manager = subprogress?.start(totalCount: 4) + manager?.complete(count: 2) + manager?.complete(count: 2) + } + + // MARK: Progress - Subprogress Interop + @Test func interopProgressParentProgressManagerChild() async throws { + // Initialize a Progress Parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressManager as Child + let p2 = overall.makeChild(withPendingUnitCount: 5) + await doSomething(subprogress: p2) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressManagerGrandchild() async throws { + // Structure: Progress with two Progress children, one of the children has a ProgressManager child + let overall = Progress.discreteProgress(totalUnitCount: 10) + + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = Progress(totalUnitCount: 1, parent: overall, pendingUnitCount: 5) + + await doSomething(subprogress: p2.makeChild(withPendingUnitCount: 1)) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressManagerGrandchildAndProgressGrandchild() async throws { + // Structure: Progress with two Progress children, one of the children has a ProgressManager child and a Progress child + let overall = Progress.discreteProgress(totalUnitCount: 10) + + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = Progress(totalUnitCount: 18) + overall.addChild(p2, withPendingUnitCount: 5) + + let p3 = await doSomethingWithProgress() + p2.addChild(p3, withPendingUnitCount: 9) + + let _ = await Task.detached { + p3.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p3.completedUnitCount = 2 + }.value + + await doSomething(subprogress: p2.makeChild(withPendingUnitCount: 9)) + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + // MARK: Progress - ProgressReporter Interop + @Test func interopProgressParentProgressReporterChild() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter as Child + let p2 = ProgressManager(totalCount: 10) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) + + p2.complete(count: 10) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressReporterChildWithNonZeroFractionCompleted() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter with CompletedCount 3 as Child + let p2 = ProgressManager(totalCount: 10) + p2.complete(count: 3) + let p2Reporter = p2.reporter + overall.addChild(p2Reporter, withPendingUnitCount: 5) + + p2.complete(count: 7) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + @Test func interopProgressParentProgressReporterGrandchild() async throws { + // Initialize a Progress parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let p1 = await doSomethingWithProgress() + overall.addChild(p1, withPendingUnitCount: 5) + + let _ = await Task.detached { + p1.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p1.completedUnitCount = 2 + }.value + + // Check if ProgressManager values propagate to Progress parent + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedUnitCount == 5) + + let p2 = await doSomethingWithProgress() + overall.addChild(p2, withPendingUnitCount: 5) + + p2.completedUnitCount = 1 + + #expect(overall.fractionCompleted == 0.75) + #expect(overall.completedUnitCount == 5) + + // Add ProgressReporter as Child + let p3 = ProgressManager(totalCount: 10) + let p3Reporter = p3.reporter + p2.addChild(p3Reporter, withPendingUnitCount: 1) + + p3.complete(count: 10) + + // Check if Progress values propagate to Progress parent + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedUnitCount == 10) + } + + // MARK: ProgressManager - Progress Interop + @Test func interopProgressManagerParentProgressChild() async throws { + // Initialize ProgressManager parent + let overallManager = ProgressManager(totalCount: 10) + + // Add ProgressManager as Child + await doSomething(subprogress: overallManager.subprogress(assigningCount: 5)) + + // Check if ProgressManager values propagate to ProgressManager parent + #expect(overallManager.fractionCompleted == 0.5) + #expect(overallManager.completedCount == 5) + + // Interop: Add Progress as Child + let p2 = await doSomethingWithProgress() + overallManager.subprogress(assigningCount: 5, to: p2) + + let _ = await Task.detached { + p2.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p2.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to ProgressRerpoter parent + #expect(overallManager.completedCount == 10) + #expect(overallManager.totalCount == 10) + #expect(overallManager.fractionCompleted == 1.0) + } + + @Test func interopProgressManagerParentProgressGrandchild() async throws { + // Initialize ProgressManager parent + let overallManager = ProgressManager(totalCount: 10) + + // Add ProgressManager as Child + await doSomething(subprogress: overallManager.subprogress(assigningCount: 5)) + + #expect(overallManager.fractionCompleted == 0.5) + #expect(overallManager.completedCount == 5) + + let p2 = overallManager.subprogress(assigningCount: 5).start(totalCount: 3) + p2.complete(count: 1) + + + let p3 = await doSomethingWithProgress() + p2.subprogress(assigningCount: 2, to: p3) + + let _ = await Task.detached { + p3.completedUnitCount = 1 + try? await Task.sleep(nanoseconds: 10000) + p3.completedUnitCount = 2 + }.value + + // Check if Progress values propagate to ProgressRerpoter parent + #expect(overallManager.completedCount == 10) + #expect(overallManager.fractionCompleted == 1.0) + } + + func getProgressWithTotalCountInitialized() -> Progress { + return Progress(totalUnitCount: 5) + } + + func receiveProgress(progress: consuming Subprogress) { + let _ = progress.start(totalCount: 5) + } + + // MARK: Behavior Consistency Tests + @Test func interopProgressManagerParentProgressChildConsistency() async throws { + let overallReporter = ProgressManager(totalCount: nil) + let child = overallReporter.subprogress(assigningCount: 5) + receiveProgress(progress: child) + #expect(overallReporter.totalCount == nil) + + let overallReporter2 = ProgressManager(totalCount: nil) + let interopChild = getProgressWithTotalCountInitialized() + overallReporter2.subprogress(assigningCount: 5, to: interopChild) + #expect(overallReporter2.totalCount == nil) + } + + @Test func interopProgressParentProgressManagerChildConsistency() async throws { + let overallProgress = Progress() + let child = Progress(totalUnitCount: 5) + overallProgress.addChild(child, withPendingUnitCount: 5) + #expect(overallProgress.totalUnitCount == 0) + + let overallProgress2 = Progress() + let interopChild = overallProgress2.makeChild(withPendingUnitCount: 5) + receiveProgress(progress: interopChild) + #expect(overallProgress2.totalUnitCount == 0) + } + + #if FOUNDATION_EXIT_TESTS + @Test func indirectParticipationOfProgressInAcyclicGraph() async throws { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + + let parentManager1 = ProgressManager(totalCount: 1) + parentManager1.assign(count: 1, to: manager.reporter) + + let parentManager2 = ProgressManager(totalCount: 1) + parentManager2.assign(count: 1, to: manager.reporter) + + let progress = Progress.discreteProgress(totalUnitCount: 4) + manager.subprogress(assigningCount: 1, to: progress) + + progress.completedUnitCount = 2 + #expect(progress.fractionCompleted == 0.5) + #expect(manager.fractionCompleted == 0.25) + #expect(parentManager1.fractionCompleted == 0.25) + #expect(parentManager2.fractionCompleted == 0.25) + + progress.addChild(parentManager1.reporter, withPendingUnitCount: 1) + } + } + #endif +} +#endif diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift new file mode 100644 index 000000000..a67578f83 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -0,0 +1,1190 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +/// Unit tests for propagation of type-safe metadata in ProgressManager tree. +@Suite("Progress Manager File Properties", .tags(.progressManager)) struct ProgressManagerAdditionalPropertiesTests { + func doFileOperation(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 100) + manager.totalFileCount = 100 + + #expect(manager.totalFileCount == 100) + + manager.complete(count: 100) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.isFinished == true) + + manager.completedFileCount = 100 + #expect(manager.completedFileCount == 100) + #expect(manager.totalFileCount == 100) + } + + @Test func discreteReporterWithFileProperties() async throws { + let fileProgressManager = ProgressManager(totalCount: 3) + await doFileOperation(reportTo: fileProgressManager.subprogress(assigningCount: 3)) + #expect(fileProgressManager.fractionCompleted == 1.0) + #expect(fileProgressManager.completedCount == 3) + #expect(fileProgressManager.isFinished == true) + #expect(fileProgressManager.totalFileCount == 0) + #expect(fileProgressManager.completedFileCount == 0) + + let summaryTotalFile = fileProgressManager.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 100) + + let summaryCompletedFile = fileProgressManager.summary(of: ProgressManager.Properties.CompletedFileCount.self) + #expect(summaryCompletedFile == 100) + } + + @Test func twoLevelTreeWithOneChildWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 10) + manager1.totalFileCount = 10 + manager1.completedFileCount = 0 + manager1.complete(count: 10) + + #expect(overall.fractionCompleted == 0.5) + + #expect(overall.totalFileCount == 0) + #expect(manager1.totalFileCount == 10) + #expect(manager1.completedFileCount == 0) + + let summaryTotalFile = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 10) + + let summaryCompletedFile = overall.summary(of: ProgressManager.Properties.CompletedFileCount.self) + #expect(summaryCompletedFile == 0) + } + + @Test func twoLevelTreeWithTwoChildrenWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 10) + + manager1.totalFileCount = 11 + manager1.completedFileCount = 0 + + let progress2 = overall.subprogress(assigningCount: 1) + let manager2 = progress2.start(totalCount: 10) + + manager2.totalFileCount = 9 + manager2.completedFileCount = 0 + + #expect(overall.fractionCompleted == 0.0) + #expect(overall.totalFileCount == 0) + #expect(overall.completedFileCount == 0) + + let summaryTotalFile = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 20) + + let summaryCompletedFile = overall.summary(of: ProgressManager.Properties.CompletedFileCount.self) + #expect(summaryCompletedFile == 0) + + // Update FileCounts + manager1.completedFileCount = 1 + + manager2.completedFileCount = 1 + + #expect(overall.completedFileCount == 0) + let summaryCompletedFileUpdated = overall.summary(of: ProgressManager.Properties.CompletedFileCount.self) + #expect(summaryCompletedFileUpdated == 2) + } + + @Test func threeLevelTreeWithFileProperties() async throws { + let overall = ProgressManager(totalCount: 1) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 5) + + + let childProgress1 = manager1.subprogress(assigningCount: 3) + let childManager1 = childProgress1.start(totalCount: nil) + childManager1.totalFileCount += 10 + #expect(childManager1.totalFileCount == 10) + + let summaryTotalFileInitial = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFileInitial == 10) + + let childProgress2 = manager1.subprogress(assigningCount: 2) + let childManager2 = childProgress2.start(totalCount: nil) + childManager2.totalFileCount += 10 + #expect(childManager2.totalFileCount == 10) + + // Tests that totalFileCount propagates to root level + #expect(overall.totalFileCount == 0) + let summaryTotalFile = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 20) + + manager1.totalFileCount += 999 + let summaryTotalFileUpdated = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFileUpdated == 1019) + } +} + +@Suite("Progress Manager Byte Properties", .tags(.progressManager)) struct ProgressManagerBytePropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 3) + manager.totalByteCount = 300000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + manager.complete(count: 1) + manager.completedByteCount += 100000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalByteCount.self) == 300000) + #expect(manager.summary(of: ProgressManager.Properties.CompletedByteCount.self) == 300000) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 200000 + manager.completedByteCount = 200000 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalByteCount.self) == 500000) + #expect(manager.summary(of: ProgressManager.Properties.CompletedByteCount.self) == 500000) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 2000 + manager.completedByteCount = 1000 + + #expect(manager.fractionCompleted == 0.5) + #expect(manager.summary(of: ProgressManager.Properties.TotalByteCount.self) == 2000) + #expect(manager.summary(of: ProgressManager.Properties.CompletedByteCount.self) == 1000) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + manager.complete(count: 1) + manager.totalByteCount = 500000 + manager.completedByteCount = 499999 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalByteCount.self) == 800000) + #expect(manager.summary(of: ProgressManager.Properties.CompletedByteCount.self) == 799999) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalByteCount = 100000 + manager.completedByteCount = 99999 + + try await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalByteCount.self) == 600000) + #expect(manager.summary(of: ProgressManager.Properties.CompletedByteCount.self) == 599999) + } +} + +@Suite("Progress Manager Throughput Properties", .tags(.progressManager)) struct ProgressManagerThroughputTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + manager.complete(count: 1) + manager.throughput += 1000 + + manager.complete(count: 1) + manager.throughput += 1000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [2000]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.throughput = 1000 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 2000]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.throughput = 1000 + manager.throughput += 2000 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [3000]) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + manager.complete(count: 1) + manager.throughput = 1000 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 2000]) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + manager.complete(count: 1) + + manager.throughput = 1000 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 1000, 2000]) + } +} + +@Suite("Progress Manager Estimated Time Remaining Properties", .tags(.progressManager)) struct ProgressManagerEstimatedTimeRemainingTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(3000) + + manager.complete(count: 1) + manager.estimatedTimeRemaining += Duration.seconds(3000) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(6000)) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(1000) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(1000)) + } + + @Test func twoLevelManagerWithFinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(1) + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(1)) + } + + @Test func twoLevelManagerWithUnfinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.estimatedTimeRemaining = Duration.seconds(200) + + var child: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 2) + child?.complete(count: 1) + child?.estimatedTimeRemaining = Duration.seconds(80000) + + #expect(manager.fractionCompleted == 0.75) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(80000)) + + child = nil + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(200)) + } + +} + +extension ProgressManager.Properties { + + var counter: Counter.Type { Counter.self } + struct Counter: Sendable, ProgressManager.Property { + + typealias Value = Int + + typealias Summary = Int + + static var key: String { return "MyApp.Counter" } + + static var defaultValue: Int { return 0 } + + static var defaultSummary: Int { return 0 } + + static func reduce(into summary: inout Int, value: Int) { + summary += value + } + + static func merge(_ summary1: Int, _ summary2: Int) -> Int { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Int, _ childSummary: Int) -> Int { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Int Properties", .tags(.progressManager)) struct ProgressManagerIntPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.counter += 10 + + manager.complete(count: 1) + manager.counter += 10 + + manager.complete(count: 1) + manager.counter += 10 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Counter.self) == 30) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.counter = 15 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Counter.self) == 45) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.counter += 10 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Counter.self) == 10) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.counter += 10 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Counter.self) == 40) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.counter += 10 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Counter.self) == 55) + } +} + +extension ProgressManager.Properties { + var byteSize: ByteSize.Type { ByteSize.self } + struct ByteSize: Sendable, ProgressManager.Property { + + typealias Value = UInt64 + + typealias Summary = UInt64 + + static var key: String { return "MyApp.ByteSize" } + + static var defaultValue: UInt64 { return 0 } + + static var defaultSummary: UInt64 { return 0 } + + static func reduce(into summary: inout UInt64, value: UInt64) { + summary += value + } + + static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { + return parentSummary + childSummary + } + } +} + + +@Suite("Progress Manager UInt64 Properties", .tags(.progressManager)) struct ProgressManagerUInt64PropertiesTests { + +func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.byteSize += 1024 + + manager.complete(count: 1) + manager.byteSize += 2048 + + manager.complete(count: 1) + manager.byteSize += 4096 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ByteSize.self) == 7168) +} + +func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.byteSize = 8192 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ByteSize.self) == 15360) +} + +@Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.byteSize += 16384 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ByteSize.self) == 16384) +} + +@Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.byteSize += 32768 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ByteSize.self) == 39936) +} + +@Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.byteSize += 65536 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ByteSize.self) == 80896) +} +} + +extension ProgressManager.Properties { + + var justADouble: JustADouble.Type { JustADouble.self } + struct JustADouble: Sendable, ProgressManager.Property { + + typealias Value = Double + + typealias Summary = Double + + static var key: String { return "MyApp.JustADouble" } + + static var defaultValue: Double { return 0.0 } + + static var defaultSummary: Double { return 0.0 } + + static func reduce(into summary: inout Double, value: Double) { + summary += value + } + + static func merge(_ summary1: Double, _ summary2: Double) -> Double { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Double, _ childSummary: Double) -> Double { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Double Properties", .tags(.progressManager)) struct ProgressManagerDoublePropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.justADouble += 10.0 + + manager.complete(count: 1) + manager.justADouble += 10.0 + + manager.complete(count: 1) + manager.justADouble += 10.0 + + #expect(manager.summary(of: ProgressManager.Properties.JustADouble.self) == 30.0) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 7.0 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: ProgressManager.Properties.JustADouble.self) == 37.0) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.JustADouble.self) == 80.0) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + try await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.JustADouble.self) == 110.0) + } + + @Test func threeLevelManager() async throws { + + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.justADouble = 80.0 + + try await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.JustADouble.self) == 117.0) + } +} + +extension ProgressManager.Properties { + + var downloadedFile: DownloadedFile.Type { DownloadedFile.self } + struct DownloadedFile: Sendable, ProgressManager.Property { + + typealias Value = String? + + typealias Summary = [String?] + + static var key: String { return "FileName" } + + static var defaultValue: String? { return "" } + + static var defaultSummary: [String?] { return [] } + + static func reduce(into summary: inout [String?], value: String?) { + summary.append(value) + } + + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [String?], _ childSummary: [String?]) -> [String?] { + return parentSummary + childSummary + } + } +} + + +@Suite("Progress Manager String (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerStringPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.downloadedFile = "Melon.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Melon.jpg"]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Cherry.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Cherry.jpg", "Melon.jpg"]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.downloadedFile = "Grape.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.downloadedFile == "Grape.jpg") + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Grape.jpg"]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Watermelon.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Watermelon.jpg", "Melon.jpg"]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.downloadedFile = "Watermelon.jpg" + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Watermelon.jpg", "Cherry.jpg", "Melon.jpg"]) + } +} + +extension ProgressManager.Properties { + + var processingFile: ProcessingFile.Type { ProcessingFile.self } + struct ProcessingFile: Sendable, ProgressManager.Property { + + typealias Value = String? + + typealias Summary = [String?] + + static var key: String { return "MyApp.ProcessingFile" } + + static var defaultValue: String? { return "" } + + static var defaultSummary: [String?] { return [] } + + static func reduce(into summary: inout [String?], value: String?) { + summary.append(value) + } + + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [String?], _ childSummary: [String?]) -> [String?] { + return parentSummary + } + } +} + +@Suite("Progress Manager String (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerStringNonRetainingProperties { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.processingFile = "Hello.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Hello.jpg"]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Hi.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Hi.jpg"]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.processingFile == "Howdy.jpg") + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Howdy.jpg"]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Howdy.jpg"]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processingFile = "Howdy.jpg" + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Howdy.jpg"]) + } +} + + +extension ProgressManager.Properties { + var imageURL: ImageURL.Type { ImageURL.self } + struct ImageURL: Sendable, ProgressManager.Property { + + typealias Value = URL? + + typealias Summary = [URL?] + + static var key: String { "MyApp.ImageURL" } + + static var defaultValue: URL? { nil } + + static var defaultSummary: [URL?] { [] } + + static func reduce(into summary: inout [URL?], value: URL?) { + summary.append(value) + } + + static func merge(_ summary1: [URL?], _ summary2: [URL?]) -> [URL?] { + summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [URL?], _ childSummary: [URL?]) -> [URL?] { + parentSummary + } + } +} + +@Suite("Progress Manager URL (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerURLProperties { + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.imageURL = URL(string: "112.jpg") + + #expect(manager.summary(of: ProgressManager.Properties.ImageURL.self) == [URL(string: "112.jpg")]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "114.jpg") + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: ProgressManager.Properties.ImageURL.self) == [URL(string: "114.jpg")]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.imageURL = URL(string: "116.jpg") + + #expect(manager.fractionCompleted == 0.0) + #expect(manager.summary(of: ProgressManager.Properties.ImageURL.self) == [URL(string: "116.jpg")]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "116.jpg") + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ImageURL.self) == [URL(string: "116.jpg")]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.imageURL = URL(string: "116.jpg") + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ImageURL.self) == [URL(string: "116.jpg")]) + } +} + +extension ProgressManager.Properties { + var totalPixelCount: TotalPixelCount.Type { TotalPixelCount.self } + struct TotalPixelCount: Sendable, ProgressManager.Property { + typealias Value = UInt64 + + typealias Summary = [UInt64] + + static var key: String { "MyApp.TotalPixelCount" } + + static var defaultValue: UInt64 { 0 } + + static var defaultSummary: [UInt64] { [] } + + static func reduce(into summary: inout [UInt64], value: UInt64) { + summary.append(value) + } + + static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { + summary1 + summary2 + } + + static func finalSummary(_ parentSummary: [UInt64], _ childSummary: [UInt64]) -> [UInt64] { + parentSummary + childSummary + } + } +} + +@Suite("Progress Manager UInt64 Array (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerUInt64ArrayProperties { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.complete(count: 1) + manager.totalPixelCount = 24 + + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [24]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 26 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [26, 24]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.totalPixelCount = 42 + + #expect(manager.fractionCompleted == 0.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [42]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 42 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [42, 24]) + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.totalPixelCount = 42 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [42, 26, 24]) + } +} + +extension ProgressManager.Properties { + var viralIndeterminate: ViralIndeterminate.Type { ViralIndeterminate.self } + struct ViralIndeterminate: Sendable, ProgressManager.Property { + typealias Value = Int + + typealias Summary = Int + + static var key: String { "MyApp.ViralIndeterminate" } + + static var defaultValue: Int { 1 } + + static var defaultSummary: Int { 1 } + + static func reduce(into summary: inout Int, value: Int) { + summary = min(summary, value) + } + + static func merge(_ summary1: Int, _ summary2: Int) -> Int { + min(summary1, summary2) + } + + static func finalSummary(_ parentSummary: Int, _ childSummary: Int) -> Int { + min(parentSummary, childSummary) + } + } +} + + +@Suite("Progress Manager Viral Indeterminate Property", .tags(.progressManager)) struct ProgressManagerViralIndeterminateProperties { + // Tests the use of additional property to virally propagate property from leaf to root + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.viralIndeterminate = 0 + + manager.complete(count: 1) + + manager.complete(count: 1) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ViralIndeterminate.self) == 0) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ViralIndeterminate.self) == 0) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ViralIndeterminate.self) == 1) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ViralIndeterminate.self) == 0) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.viralIndeterminate = 1 + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ViralIndeterminate.self) == 0) + } +} + +extension ProgressManager.Properties { + var processTime: ProcessTime.Type { ProcessTime.self } + struct ProcessTime: Sendable, ProgressManager.Property { + + typealias Value = Duration + + typealias Summary = Duration + + static var key: String { return "MyApp.ProcessTime" } + + static var defaultValue: Duration { return .zero } + + static var defaultSummary: Duration { return .zero } + + static func reduce(into summary: inout Duration, value: Duration) { + summary += value + } + + static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration { + return summary1 + summary2 + } + + static func finalSummary(_ parentSummary: Duration, _ childSummary: Duration) -> Duration { + return parentSummary + childSummary + } + } +} + +@Suite("Progress Manager Duration Properties", .tags(.progressManager)) struct ProgressManagerDurationPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(10) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(15) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(25) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(50)) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.complete(count: 1) + + manager.processTime = Duration.seconds(30) + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(80)) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.processTime += Duration.milliseconds(500) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.milliseconds(500)) + } + + @Test func twoLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime += Duration.seconds(120) + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(170)) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime += Duration.microseconds(1000000) // 1 second + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(81)) + } + + @Test func zeroDurationHandling() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + manager.processTime = Duration.zero + + let childProgress = manager.subprogress(assigningCount: 1) + let childManager = childProgress.start(totalCount: 1) + + childManager.complete(count: 1) + childManager.processTime = Duration.seconds(42) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(42)) + } + + @Test func negativeDurationHandling() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + // Test with negative duration (though this might be unusual in practice) + manager.processTime = Duration.seconds(-5) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(-5)) + } + + @Test func mixedDurationUnits() async throws { + let manager = ProgressManager(totalCount: 3) + + manager.complete(count: 1) + manager.processTime = Duration.seconds(1) // 1 second + + manager.complete(count: 1) + manager.processTime += Duration.milliseconds(500) // + 0.5 seconds + + manager.complete(count: 1) + manager.processTime += Duration.microseconds(500000) // + 0.5 seconds + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.ProcessTime.self) == Duration.seconds(2)) + } +} + diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift new file mode 100644 index 000000000..3fdc7cd33 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -0,0 +1,795 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +extension Tag { + @Tag static var progressManager: Self +} + +/// Unit tests for basic functionalities of ProgressManager +@Suite("Progress Manager", .tags(.progressManager)) struct ProgressManagerTests { + /// MARK: Helper methods that report progress + func doBasicOperationV1(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 8) + for i in 1...8 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(8)) + } + } + + func doBasicOperationV2(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 7) + for i in 1...7 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(7)) + } + } + + func doBasicOperationV3(reportTo subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 11) + for i in 1...11 { + manager.complete(count: 1) + #expect(manager.completedCount == i) + #expect(manager.fractionCompleted == Double(i) / Double(11)) + } + } + + /// MARK: Tests calculations based on change in totalCount + @Test func totalCountNil() async throws { + let overall = ProgressManager(totalCount: nil) + overall.complete(count: 10) + #expect(overall.completedCount == 10) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.totalCount == nil) + } + + @Test func totalCountReset() async throws { + let overall = ProgressManager(totalCount: 10) + overall.complete(count: 5) + #expect(overall.completedCount == 5) + #expect(overall.totalCount == 10) + #expect(overall.fractionCompleted == 0.5) + #expect(overall.isIndeterminate == false) + + overall.setCounts { _, total in + total = nil + } + overall.complete(count: 1) + #expect(overall.completedCount == 6) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + overall.setCounts { _, total in + total = 12 + } + overall.complete(count: 2) + #expect(overall.completedCount == 8) + #expect(overall.totalCount == 12) + #expect(overall.fractionCompleted == Double(8) / Double(12)) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == false) + } + + @Test func totalCountNilWithChild() async throws { + let overall = ProgressManager(totalCount: nil) + #expect(overall.completedCount == 0) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + let progress1 = overall.subprogress(assigningCount: 2) + let manager1 = progress1.start(totalCount: 1) + + manager1.complete(count: 1) + #expect(manager1.totalCount == 1) + #expect(manager1.completedCount == 1) + #expect(manager1.fractionCompleted == 1.0) + #expect(manager1.isIndeterminate == false) + #expect(manager1.isFinished == true) + + #expect(overall.completedCount == 2) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + overall.setCounts { _, total in + total = 5 + } + #expect(overall.completedCount == 2) + #expect(overall.totalCount == 5) + #expect(overall.fractionCompleted == 0.4) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == false) + } + + @Test func totalCountFinishesWithLessCompletedCount() async throws { + let overall = ProgressManager(totalCount: 10) + overall.complete(count: 5) + + let progress1 = overall.subprogress(assigningCount: 8) + let manager1 = progress1.start(totalCount: 1) + manager1.complete(count: 1) + + #expect(overall.completedCount == 13) + #expect(overall.totalCount == 10) + #expect(overall.fractionCompleted == 1.3) + #expect(overall.isIndeterminate == false) + #expect(overall.isFinished == true) + } + + @Test func childTotalCountReset() async throws { + let overall = ProgressManager(totalCount: 1) + + let childManager = overall.subprogress(assigningCount: 1).start(totalCount: 4) + childManager.complete(count: 2) + + #expect(overall.fractionCompleted == 0.5) + #expect(childManager.isIndeterminate == false) + + childManager.setCounts { _, total in + total = nil + } + + #expect(overall.fractionCompleted == 0.0) + #expect(childManager.isIndeterminate == true) + #expect(childManager.completedCount == 2) + + childManager.setCounts { _, total in + total = 5 + } + childManager.complete(count: 2) + + #expect(overall.fractionCompleted == 0.8) + #expect(childManager.completedCount == 4) + #expect(childManager.isIndeterminate == false) + + childManager.complete(count: 1) + #expect(overall.fractionCompleted == 1.0) + } + + /// MARK: Tests single-level tree + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 3) + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 3)) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 3) + #expect(manager.isFinished == true) + } + + /// MARK: Tests multiple-level trees + @Test func emptyDiscreteManager() async throws { + let manager = ProgressManager(totalCount: nil) + #expect(manager.isIndeterminate == true) + + manager.setCounts { _, total in + total = 10 + } + #expect(manager.isIndeterminate == false) + #expect(manager.totalCount == 10) + + await doBasicOperationV1(reportTo: manager.subprogress(assigningCount: 10)) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 10) + #expect(manager.isFinished == true) + } + + @Test func twoLevelTreeWithTwoChildren() async throws { + let overall = ProgressManager(totalCount: 2) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount: 1)) + #expect(overall.fractionCompleted == 0.5) + #expect(overall.completedCount == 1) + #expect(overall.isFinished == false) + #expect(overall.isIndeterminate == false) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount: 1)) + #expect(overall.fractionCompleted == 1.0) + #expect(overall.completedCount == 2) + #expect(overall.isFinished == true) + #expect(overall.isIndeterminate == false) + } + + @Test func twoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { + let overall = ProgressManager(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let manager1 = progress1.start(totalCount: 5) + manager1.complete(count: 5) + + let progress2 = overall.subprogress(assigningCount: 1) + let manager2 = progress2.start(totalCount: 5) + manager2.totalFileCount = 10 + + #expect(overall.fractionCompleted == 0.5) + // Parent is expected to get totalFileCount from one of the children with a totalFileCount + #expect(overall.totalFileCount == 0) + } + + @Test func twoLevelTreeWithMultipleChildren() async throws { + let overall = ProgressManager(totalCount: 3) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(1) / Double(3)) + #expect(overall.completedCount == 1) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(2) / Double(3)) + #expect(overall.completedCount == 2) + + await doBasicOperationV3(reportTo: overall.subprogress(assigningCount:1)) + #expect(overall.fractionCompleted == Double(3) / Double(3)) + #expect(overall.completedCount == 3) + } + + @Test func threeLevelTree() async throws { + let overall = ProgressManager(totalCount: 100) + #expect(overall.fractionCompleted == 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let manager1 = child1.start(totalCount: 100) + + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) + + #expect(overall.fractionCompleted == 0.0) + + grandchildManager1.complete(count: 50) + #expect(manager1.fractionCompleted == 0.5) + #expect(overall.fractionCompleted == 0.5) + + grandchildManager1.complete(count: 50) + #expect(manager1.fractionCompleted == 1.0) + #expect(overall.fractionCompleted == 1.0) + + #expect(grandchildManager1.isFinished == true) + #expect(manager1.isFinished == true) + #expect(overall.isFinished == true) + } + + @Test func fourLevelTree() async throws { + let overall = ProgressManager(totalCount: 100) + #expect(overall.fractionCompleted == 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let manager1 = child1.start(totalCount: 100) + + let grandchild1 = manager1.subprogress(assigningCount: 100) + let grandchildManager1 = grandchild1.start(totalCount: 100) + + #expect(overall.fractionCompleted == 0.0) + + let greatGrandchild1 = grandchildManager1.subprogress(assigningCount: 100) + let greatGrandchildManager1 = greatGrandchild1.start(totalCount: 100) + + greatGrandchildManager1.complete(count: 50) + #expect(overall.fractionCompleted == 0.5) + + greatGrandchildManager1.complete(count: 50) + #expect(overall.fractionCompleted == 1.0) + + #expect(greatGrandchildManager1.isFinished == true) + #expect(grandchildManager1.isFinished == true) + #expect(manager1.isFinished == true) + #expect(overall.isFinished == true) + } + + func doSomething(amount: Int, subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: amount) + for _ in 1...amount { + manager.complete(count: 1) + } + } + + @Test func fiveThreadsMutatingAndReading() async throws { + let manager = ProgressManager(totalCount: 10) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + await doSomething(amount: 5, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 8, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 7, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + await doSomething(amount: 6, subprogress: manager.subprogress(assigningCount: 1)) + } + + group.addTask { + #expect(manager.fractionCompleted <= 0.4) + } + } + } + + // MARK: Test deinit behavior + func makeUnfinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + manager.complete(count: 2) + #expect(manager.fractionCompleted == Double(2) / Double(3)) + } + + func makeFinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + manager.complete(count: 2) + } + + @Test func unfinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + await makeUnfinishedChild(subprogress: manager.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func unfinishedGrandchild() async throws { + let manager = ProgressManager(totalCount: 1) + + let child = manager.subprogress(assigningCount: 1).start(totalCount: 1) + + await makeUnfinishedChild(subprogress: child.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func unfinishedGreatGrandchild() async throws { + let manager = ProgressManager(totalCount: 1) + + let child = manager.subprogress(assigningCount: 1).start(totalCount: 1) + + let grandchild = child.subprogress(assigningCount: 1).start(totalCount: 1) + + await makeUnfinishedChild(subprogress: grandchild.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func finishedChildUnreadBeforeDeinit() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + await makeFinishedChild(subprogress: manager.subprogress(assigningCount: 1)) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func finishedChildReadBeforeDeinit() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + #expect(manager.fractionCompleted == 0.5) + + var child: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 1) + child?.complete(count: 1) + #expect(manager.fractionCompleted == 1.0) + + child = nil + #expect(manager.fractionCompleted == 1.0) + } + + @Test func uninitializedSubprogress() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.complete(count: 1) + + var subprogress: Subprogress? = manager.subprogress(assigningCount: 1) + #expect(manager.fractionCompleted == 0.5) + + subprogress = nil + #expect(manager.fractionCompleted == 1.0) + } + + @Test func deallocatedChild() async throws { + let manager = ProgressManager(totalCount: 100) + + var child: ProgressManager? = manager.subprogress(assigningCount: 50).start(totalCount: 10) + child!.complete(count: 5) + + let fractionBeforeDeallocation = manager.fractionCompleted + #expect(fractionBeforeDeallocation == 0.25) + + child = nil + + for _ in 1...10 { + _ = manager.fractionCompleted + } + + let fractionAfterDeallocation = manager.fractionCompleted + + #expect(fractionAfterDeallocation == 0.5, "Deallocated child should be assumed completed.") + + manager.complete(count: 50) + #expect(manager.fractionCompleted == 1.0) + } +} + +// MARK: - Thread Safety and Concurrent Access Tests +@Suite("Progress Manager Thread Safety Tests", .tags(.progressManager)) struct ProgressManagerThreadSafetyTests { + + @Test func concurrentBasicPropertiesAccess() async throws { + let manager = ProgressManager(totalCount: 10) + manager.complete(count: 5) + + await withThrowingTaskGroup(of: Void.self) { group in + + group.addTask { + for _ in 1...10 { + let fraction = manager.fractionCompleted + #expect(fraction == 0.5) + } + } + + group.addTask { + for _ in 1...10 { + let completed = manager.completedCount + #expect(completed == 5) + } + } + + group.addTask { + for _ in 1...10 { + let total = manager.totalCount + #expect(total == 10) + } + } + + group.addTask { + for _ in 1...10 { + let isFinished = manager.isFinished + #expect(isFinished == false) + } + } + + group.addTask { + for _ in 1...10 { + let isIndeterminate = manager.isIndeterminate + #expect(isIndeterminate == false) + } + } + } + } + + @Test func concurrentMultipleChildrenUpdatesAndParentReads() async throws { + let manager = ProgressManager(totalCount: 100) + let child1 = manager.subprogress(assigningCount: 30).start(totalCount: 10) + let child2 = manager.subprogress(assigningCount: 40).start(totalCount: 8) + let child3 = manager.subprogress(assigningCount: 30).start(totalCount: 6) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...10 { + child1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...8 { + child2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...6 { + child3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...50 { + let _ = manager.fractionCompleted + let _ = manager.completedCount + let _ = manager.isFinished + } + } + + group.addTask { + for _ in 1...30 { + let _ = child1.fractionCompleted + let _ = child2.completedCount + let _ = child3.isFinished + } + } + } + + #expect(child1.isFinished == true) + #expect(child2.isFinished == true) + #expect(child3.isFinished == true) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func concurrentSingleChildUpdatesAndParentReads() async throws { + let manager = ProgressManager(totalCount: 50) + let child = manager.subprogress(assigningCount: 50).start(totalCount: 100) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for i in 1...100 { + child.complete(count: 1) + if i % 10 == 0 { + try? await Task.sleep(nanoseconds: 1_000_000) + } + } + } + + group.addTask { + for _ in 1...200 { + let _ = manager.fractionCompleted + let _ = manager.completedCount + let _ = manager.totalCount + let _ = manager.isFinished + let _ = manager.isIndeterminate + } + } + + group.addTask { + for _ in 1...150 { + let _ = child.fractionCompleted + let _ = child.completedCount + let _ = child.isFinished + } + } + } + + #expect(child.isFinished == true) + #expect(manager.fractionCompleted == 1.0) + } + + @Test func concurrentGrandchildrenUpdates() async throws { + let parent = ProgressManager(totalCount: 60) + let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) + let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) + let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) + + let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) + let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) + let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...4 { + grandchild1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + grandchild2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...2 { + grandchild3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...5 { + child1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...4 { + child2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + child3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...100 { + let _ = parent.fractionCompleted + let _ = child1.fractionCompleted + let _ = grandchild1.completedCount + } + } + } + + #expect(grandchild1.isFinished == true) + #expect(grandchild2.isFinished == true) + #expect(grandchild3.isFinished == true) + #expect(parent.isFinished == true) + } + + @Test func concurrentReadDuringIndeterminateToDeterminateTransition() async throws { + let manager = ProgressManager(totalCount: nil) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...50 { + let _ = manager.fractionCompleted + let _ = manager.isIndeterminate + } + } + + group.addTask { + for _ in 1...10 { + manager.complete(count: 1) + } + } + + // Task 3: Change to determinate after a delay + group.addTask { + try? await Task.sleep(nanoseconds: 1_000_000) + manager.setCounts { _, total in + total = 20 + } + + for _ in 1...30 { + let _ = manager.fractionCompleted + let _ = manager.isIndeterminate + } + } + } + + #expect(manager.totalCount == 20) + #expect(manager.completedCount == 10) + #expect(manager.isIndeterminate == false) + } + + @Test func concurrentReadDuringExcessiveCompletion() async throws { + let manager = ProgressManager(totalCount: 5) + + await withThrowingTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...20 { + manager.complete(count: 1) + try? await Task.sleep(nanoseconds: 100_000) + } + } + + group.addTask { + for _ in 1...100 { + let fraction = manager.fractionCompleted + let completed = manager.completedCount + + #expect(completed >= 0 && completed <= 20) + #expect(fraction >= 0.0 && fraction <= 4.0) + } + } + } + + #expect(manager.completedCount == 20) + #expect(manager.fractionCompleted == 4.0) + #expect(manager.isFinished == true) + } + + @Test func concurrentChildrenDeinitializationAndParentReads() async throws { + let manager = ProgressManager(totalCount: 100) + + await withThrowingTaskGroup(of: Void.self) { group in + // Create and destroy children rapidly + for batch in 1...10 { + group.addTask { + for i in 1...5 { + func createAndDestroyChild() { + let child = manager.subprogress(assigningCount: 2).start(totalCount: 3) + child.complete(count: 2 + (i % 2)) // Complete 2 or 3 + // child deinits here + } + + createAndDestroyChild() + try? await Task.sleep(nanoseconds: 200_000 * UInt64(batch)) + } + } + } + + // Continuously read manager state during child lifecycle + group.addTask { + for _ in 1...300 { + let fraction = manager.fractionCompleted + let completed = manager.completedCount + + // Properties should be stable and valid + #expect(fraction >= 0.0) + #expect(completed >= 0) + + try? await Task.sleep(nanoseconds: 50_000) + } + } + } + + // Manager should reach completion + #expect(manager.fractionCompleted == 1.0) + #expect(manager.completedCount == 100) + } + + @Test func concurrentReadAndWriteAndCycleDetection() async throws { + let manager1 = ProgressManager(totalCount: 10) + let manager2 = ProgressManager(totalCount: 10) + let manager3 = ProgressManager(totalCount: 10) + + // Create initial chain: manager1 -> manager2 -> manager3 + manager1.assign(count: 5, to: manager2.reporter) + manager2.assign(count: 5, to: manager3.reporter) + + await withTaskGroup(of: Void.self) { group in + // Task 1: Try to detect cycles continuously + group.addTask { + for _ in 1...50 { + let wouldCycle1 = manager1.isCycle(reporter: manager3.reporter) + let wouldCycle2 = manager2.isCycle(reporter: manager1.reporter) + let wouldCycle3 = manager3.isCycle(reporter: manager2.reporter) + + #expect(wouldCycle1 == false) // No cycle yet + #expect(wouldCycle2 == true) // Would create cycle + #expect(wouldCycle3 == true) // Would create cycle + } + } + + // Task 2: Complete work in all managers + group.addTask { + for _ in 1...5 { + manager1.complete(count: 1) + manager2.complete(count: 1) + manager3.complete(count: 1) + } + } + + // Task 3: Access properties during cycle detection + group.addTask { + for _ in 1...100 { + let _ = manager1.fractionCompleted + let _ = manager2.completedCount + let _ = manager3.isFinished + } + } + } + } + + @Test func concurrentSubprogressCreation() async throws { + let manager = ProgressManager(totalCount: 1000) + + await withThrowingTaskGroup(of: Void.self) { group in + // Create 20 concurrent tasks, each creating multiple subprogresses + for _ in 1...20 { + group.addTask { + for i in 1...10 { + let child = manager.subprogress(assigningCount: 5).start(totalCount: 4) + child.complete(count: 4) + + // Immediately access properties + let _ = child.fractionCompleted + let _ = manager.fractionCompleted + + try? await Task.sleep(nanoseconds: 100_000 * UInt64(i)) + } + } + } + } + + #expect(manager.completedCount == 1000) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift new file mode 100644 index 000000000..666a46383 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +import Testing + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +@Suite("Progress Reporter", .tags(.progressManager)) struct ProgressReporterTests { + @Test func observeProgressReporter() { + let manager = ProgressManager(totalCount: 3) + + let reporter = manager.reporter + + manager.complete(count: 1) + #expect(reporter.completedCount == 1) + + manager.complete(count: 1) + #expect(reporter.completedCount == 2) + + manager.complete(count: 1) + #expect(reporter.completedCount == 3) + + let fileCount = reporter.totalFileCount + #expect(fileCount == 0) + + manager.totalFileCount = 6 + #expect(reporter.totalFileCount == 6) + + let summaryTotalFile = manager.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 6) + } + + @Test func testAddProgressReporterAsChild() { + let manager = ProgressManager(totalCount: 2) + + let reporter = manager.reporter + + let altManager1 = ProgressManager(totalCount: 4) + altManager1.assign(count: 1, to: reporter) + + let altManager2 = ProgressManager(totalCount: 5) + altManager2.assign(count: 2, to: reporter) + + manager.complete(count: 1) + #expect(altManager1.fractionCompleted == 0.125) + #expect(altManager2.fractionCompleted == 0.2) + + manager.complete(count: 1) + #expect(altManager1.fractionCompleted == 0.25) + #expect(altManager2.fractionCompleted == 0.4) + } + + @Test func testAssignToProgressReporterThenSetTotalCount() { + let overall = ProgressManager(totalCount: nil) + + let child1 = ProgressManager(totalCount: 10) + overall.assign(count: 10, to: child1.reporter) + child1.complete(count: 5) + + let child2 = ProgressManager(totalCount: 20) + overall.assign(count: 20, to: child2.reporter) + child2.complete(count: 20) + + overall.setCounts { _, total in + total = 30 + } + #expect(overall.completedCount == 20) + #expect(overall.fractionCompleted == Double(25) / Double(30)) + + child1.complete(count: 5) + + #expect(overall.completedCount == 30) + #expect(overall.fractionCompleted == 1.0) + } + + @Test func testMakeSubprogressThenSetTotalCount() async { + let overall = ProgressManager(totalCount: nil) + + let reporter1 = await dummy(index: 1, subprogress: overall.subprogress(assigningCount: 10)) + + let reporter2 = await dummy(index: 2, subprogress: overall.subprogress(assigningCount: 20)) + + #expect(reporter1.fractionCompleted == 0.5) + + #expect(reporter2.fractionCompleted == 0.5) + + overall.setCounts { _, total in + total = 30 + } + + #expect(overall.totalCount == 30) + #expect(overall.fractionCompleted == 0.5) + } + + func dummy(index: Int, subprogress: consuming Subprogress) async -> ProgressReporter { + let manager = subprogress.start(totalCount: index * 10) + + manager.complete(count: (index * 10) / 2) + + return manager.reporter + } + + #if FOUNDATION_EXIT_TESTS + @Test func testProgressReporterDirectCycleDetection() async { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + manager.assign(count: 1, to: manager.reporter) + } + } + + @Test func testProgressReporterIndirectCycleDetection() async throws { + await #expect(processExitsWith: .failure) { + let manager = ProgressManager(totalCount: 2) + + let altManager = ProgressManager(totalCount: 1) + altManager.assign(count: 1, to: manager.reporter) + + manager.assign(count: 1, to: altManager.reporter) + } + } + + @Test func testProgressReporterNestedCycleDetection() async throws { + + await #expect(processExitsWith: .failure) { + let manager1 = ProgressManager(totalCount: 1) + + let manager2 = ProgressManager(totalCount: 2) + manager1.assign(count: 1, to: manager2.reporter) + + let manager3 = ProgressManager(totalCount: 3) + manager2.assign(count: 1, to: manager3.reporter) + + manager3.assign(count: 1, to: manager1.reporter) + + } + } + #endif +} From 265e19762c37dfde70efc246c6d5ed37947dbeb8 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 6 Oct 2025 16:38:52 -0700 Subject: [PATCH 2/5] temporarily disable flaky unit test --- .../ProgressManagerTests.swift | 122 +++++++++--------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 3fdc7cd33..b70ef1105 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -561,67 +561,67 @@ extension Tag { #expect(manager.fractionCompleted == 1.0) } - @Test func concurrentGrandchildrenUpdates() async throws { - let parent = ProgressManager(totalCount: 60) - let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) - let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) - let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) - - let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) - let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) - let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) - - await withTaskGroup(of: Void.self) { group in - group.addTask { - for _ in 1...4 { - grandchild1.complete(count: 1) - } - } - - group.addTask { - for _ in 1...3 { - grandchild2.complete(count: 1) - } - } - - group.addTask { - for _ in 1...2 { - grandchild3.complete(count: 1) - } - } - - group.addTask { - for _ in 1...5 { - child1.complete(count: 1) - } - } - - group.addTask { - for _ in 1...4 { - child2.complete(count: 1) - } - } - - group.addTask { - for _ in 1...3 { - child3.complete(count: 1) - } - } - - group.addTask { - for _ in 1...100 { - let _ = parent.fractionCompleted - let _ = child1.fractionCompleted - let _ = grandchild1.completedCount - } - } - } - - #expect(grandchild1.isFinished == true) - #expect(grandchild2.isFinished == true) - #expect(grandchild3.isFinished == true) - #expect(parent.isFinished == true) - } +// @Test func concurrentGrandchildrenUpdates() async throws { +// let parent = ProgressManager(totalCount: 60) +// let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) +// let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) +// let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) +// +// let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) +// let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) +// let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) +// +// await withTaskGroup(of: Void.self) { group in +// group.addTask { +// for _ in 1...4 { +// grandchild1.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...3 { +// grandchild2.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...2 { +// grandchild3.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...5 { +// child1.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...4 { +// child2.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...3 { +// child3.complete(count: 1) +// } +// } +// +// group.addTask { +// for _ in 1...100 { +// let _ = parent.fractionCompleted +// let _ = child1.fractionCompleted +// let _ = grandchild1.completedCount +// } +// } +// } +// +// #expect(grandchild1.isFinished == true) +// #expect(grandchild2.isFinished == true) +// #expect(grandchild3.isFinished == true) +// #expect(parent.isFinished == true) +// } @Test func concurrentReadDuringIndeterminateToDeterminateTransition() async throws { let manager = ProgressManager(totalCount: nil) From c2ea52ea587ce4d36a931d69082f1877b4fbcea5 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 7 Oct 2025 10:26:23 -0700 Subject: [PATCH 3/5] disable flaky tests --- .../ProgressManagerTests.swift | 126 +++++++++--------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index b70ef1105..aa6f85a48 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -476,7 +476,7 @@ extension Tag { } } - @Test func concurrentMultipleChildrenUpdatesAndParentReads() async throws { + @Test(.disabled()) func concurrentMultipleChildrenUpdatesAndParentReads() async throws { let manager = ProgressManager(totalCount: 100) let child1 = manager.subprogress(assigningCount: 30).start(totalCount: 10) let child2 = manager.subprogress(assigningCount: 40).start(totalCount: 8) @@ -561,67 +561,67 @@ extension Tag { #expect(manager.fractionCompleted == 1.0) } -// @Test func concurrentGrandchildrenUpdates() async throws { -// let parent = ProgressManager(totalCount: 60) -// let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) -// let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) -// let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) -// -// let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) -// let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) -// let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) -// -// await withTaskGroup(of: Void.self) { group in -// group.addTask { -// for _ in 1...4 { -// grandchild1.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...3 { -// grandchild2.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...2 { -// grandchild3.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...5 { -// child1.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...4 { -// child2.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...3 { -// child3.complete(count: 1) -// } -// } -// -// group.addTask { -// for _ in 1...100 { -// let _ = parent.fractionCompleted -// let _ = child1.fractionCompleted -// let _ = grandchild1.completedCount -// } -// } -// } -// -// #expect(grandchild1.isFinished == true) -// #expect(grandchild2.isFinished == true) -// #expect(grandchild3.isFinished == true) -// #expect(parent.isFinished == true) -// } + @Test(.disabled()) func concurrentGrandchildrenUpdates() async throws { + let parent = ProgressManager(totalCount: 60) + let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) + let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) + let child3 = parent.subprogress(assigningCount: 20).start(totalCount: 6) + + let grandchild1 = child1.subprogress(assigningCount: 5).start(totalCount: 4) + let grandchild2 = child2.subprogress(assigningCount: 4).start(totalCount: 3) + let grandchild3 = child3.subprogress(assigningCount: 3).start(totalCount: 2) + + await withTaskGroup(of: Void.self) { group in + group.addTask { + for _ in 1...4 { + grandchild1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + grandchild2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...2 { + grandchild3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...5 { + child1.complete(count: 1) + } + } + + group.addTask { + for _ in 1...4 { + child2.complete(count: 1) + } + } + + group.addTask { + for _ in 1...3 { + child3.complete(count: 1) + } + } + + group.addTask { + for _ in 1...100 { + let _ = parent.fractionCompleted + let _ = child1.fractionCompleted + let _ = grandchild1.completedCount + } + } + } + + #expect(grandchild1.isFinished == true) + #expect(grandchild2.isFinished == true) + #expect(grandchild3.isFinished == true) + #expect(parent.isFinished == true) + } @Test func concurrentReadDuringIndeterminateToDeterminateTransition() async throws { let manager = ProgressManager(totalCount: nil) @@ -769,7 +769,7 @@ extension Tag { } } - @Test func concurrentSubprogressCreation() async throws { + @Test(.disabled()) func concurrentSubprogressCreation() async throws { let manager = ProgressManager(totalCount: 1000) await withThrowingTaskGroup(of: Void.self) { group in From abcd70f8234014af24782cfa9a6e7f4a3b10c592 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 7 Oct 2025 10:33:49 -0700 Subject: [PATCH 4/5] rerun From 9abdc7bbb9cf85c5354be99941c1c5d516535d5a Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 7 Oct 2025 15:22:16 -0700 Subject: [PATCH 5/5] dirty bit clearing fix + re-enable tests --- .../ProgressManager/ProgressManager+State.swift | 2 -- .../ProgressManager/ProgressManagerTests.swift | 6 +++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index aa894f09c..6810d65fc 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -310,8 +310,6 @@ extension ProgressManager { if update.updatedFraction.isFinished && !currentWasFinished { selfFraction.completed += update.assignedCount } - - children[update.index].isFractionDirty = false } } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index aa6f85a48..3fdc7cd33 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -476,7 +476,7 @@ extension Tag { } } - @Test(.disabled()) func concurrentMultipleChildrenUpdatesAndParentReads() async throws { + @Test func concurrentMultipleChildrenUpdatesAndParentReads() async throws { let manager = ProgressManager(totalCount: 100) let child1 = manager.subprogress(assigningCount: 30).start(totalCount: 10) let child2 = manager.subprogress(assigningCount: 40).start(totalCount: 8) @@ -561,7 +561,7 @@ extension Tag { #expect(manager.fractionCompleted == 1.0) } - @Test(.disabled()) func concurrentGrandchildrenUpdates() async throws { + @Test func concurrentGrandchildrenUpdates() async throws { let parent = ProgressManager(totalCount: 60) let child1 = parent.subprogress(assigningCount: 20).start(totalCount: 10) let child2 = parent.subprogress(assigningCount: 20).start(totalCount: 8) @@ -769,7 +769,7 @@ extension Tag { } } - @Test(.disabled()) func concurrentSubprogressCreation() async throws { + @Test func concurrentSubprogressCreation() async throws { let manager = ProgressManager(totalCount: 1000) await withThrowingTaskGroup(of: Void.self) { group in