From fc598d58349e3f2b9b21c7fb4b7598b550aa72e8 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 8 Jul 2025 13:31:05 -0700 Subject: [PATCH 01/24] ProgressManager v6 implementation restore conflict files Restore files to 26.E version Restore files to 26.E version restore preferences.h file restore preferences.m file restore attributedString file revert attributed string remove additional lines change to UInt64 where needed add if FOUNDATION_FRAMEWORK in relevant areas add if FOUNDATION_FRAMEWORK in relevant areas more #FOUNDATION_FRAMEWORK check remove exclusion from Internationalization typo --- Package.swift | 5 +- Sources/FoundationEssentials/CMakeLists.txt | 1 + .../ProgressManager/CMakeLists.txt | 23 + .../ProgressManager/ProgressFraction.swift | 337 ++++++++ .../ProgressManager+Interop.swift | 270 +++++++ ...ProgressManager+Properties+Accessors.swift | 496 ++++++++++++ ...ogressManager+Properties+Definitions.swift | 261 +++++++ .../ProgressManager+Properties+Helpers.swift | 562 ++++++++++++++ .../ProgressManager+State.swift | 181 +++++ .../ProgressManager/ProgressManager.swift | 413 ++++++++++ .../ProgressManager/ProgressReporter.swift | 178 +++++ .../ProgressManager/Subprogress.swift | 83 ++ .../ProgressFractionTests.swift | 167 ++++ .../ProgressManagerInteropTests.swift | 332 ++++++++ .../ProgressManagerPropertiesTests.swift | 734 ++++++++++++++++++ .../ProgressManagerTests.swift | 348 +++++++++ .../ProgressReporterTests.swift | 154 ++++ 17 files changed, 4543 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..be1a836e3 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressFraction.swift @@ -0,0 +1,337 @@ +//===----------------------------------------------------------------------===// +// +// 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? + 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 + } + } + + static private func _math(lhs: ProgressFraction, rhs: ProgressFraction, whichOperator: (_ lhs : Double, _ rhs : Double) -> Double, whichOverflow : (_ lhs: Int, _ rhs: Int) -> (Int, overflow: Bool)) -> 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: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + //TODO: rdar://148758226 Overflow check + if let lcm = _leastCommonMultiple(lhsTotal, rhsTotal) { + let result = whichOverflow(lhs.completed * (lcm / lhsTotal), rhs.completed * (lcm / rhsTotal)) + if result.overflow { + return ProgressFraction(double: whichOperator(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: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + + if let lcm = _leastCommonMultiple(lhsSimplifiedTotal, rhsSimplifiedTotal) { + let result = whichOverflow(lhsSimplified.completed * (lcm / lhsSimplifiedTotal), rhsSimplified.completed * (lcm / rhsSimplifiedTotal)) + if result.overflow { + // Use original lhs/rhs here + return ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } else { + return ProgressFraction(completed: result.0, total: lcm) + } + } else { + // Still overflow + return ProgressFraction(double: whichOperator(lhs.fractionCompleted, rhs.fractionCompleted), overflow: true) + } + } + } + + static internal func +(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, whichOperator: +, whichOverflow: { $0.addingReportingOverflow($1) }) + } + + static internal func -(lhs: ProgressFraction, rhs: ProgressFraction) -> ProgressFraction { + return _math(lhs: lhs, rhs: rhs, whichOperator: -, whichOverflow: { $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..b76acbc64 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -0,0 +1,270 @@ +//===----------------------------------------------------------------------===// +// +// 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 +internal import Synchronization + +//MARK: Progress Parent - Subprogress / ProgressReporter Child Interop +@available(FoundationPreview 6.2, *) +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, + portionOfParent: 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 { + if self._parent() == nil { + return false + } + + if !(self._parent() is NSProgressBridge) { + return self._parent().isCycle(reporter: reporter) + } + + let unwrappedParent = (self._parent() as? NSProgressBridge)?.manager + if let unwrappedParent = unwrappedParent { + if unwrappedParent === reporter.manager { + return true + } + let updatedVisited = visited.union([unwrappedParent]) + return unwrappedParent.isCycleInterop(reporter: reporter, visited: updatedVisited) + } + return false + } +} + +@available(FoundationPreview 6.2, *) +//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, + portion: count + ) + + // Add bridge as a parent + progress._setParent(progressBridge, portion: Int64(count)) + + // Store bridge + self.addBridge(nsProgressBridge: progressBridge) + } +} + +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 + } + + switch observerState { + case .fractionUpdated(let totalCount, let completedCount): + // This needs to change totalUnitCount before completedUnitCount otherwise progressBridge will finish and mess up the math + self.progressBridge.totalUnitCount = Int64(totalCount) + self.progressBridge.completedUnitCount = Int64(completedCount) + } + } + } +} + +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 + } + + switch observerState { + case .fractionUpdated(let totalCount, let completedCount): + self.progressBridge.totalUnitCount = Int64(totalCount) + self.progressBridge.completedUnitCount = Int64(completedCount) + } + } + } + +} + +internal final class NSProgressBridge: Progress, @unchecked Sendable { + + internal let manager: ProgressManager + internal let managerBridge: ProgressManager + internal let progress: Progress + + init(manager: ProgressManager, progress: Progress, portion: Int) { + self.manager = manager + self.managerBridge = ProgressManager(totalCount: Int(progress.totalUnitCount)) + self.progress = progress + super.init(parent: nil, userInfo: nil) + + managerBridge.withProperties { properties in + properties.completedCount = Int(progress.completedUnitCount) + } + + let position = manager.addChild( + child: managerBridge, + portion: portion, + childFraction: ProgressFraction(completed: Int(completedUnitCount), total: Int(totalUnitCount)) + ) + managerBridge.addParent(parent: 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.withProperties { properties in + properties.totalCount = Int(fraction.next.total) + properties.completedCount = Int(fraction.next.completed) + } + + managerBridge.markSelfDirty() + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressManager { + // Keeping this as an enum in case we have other states to track in the future. + internal enum ObserverState { + case fractionUpdated(totalCount: Int, completedCount: Int) + } + + internal struct InteropObservation { + let subprogressBridge: SubprogressBridge? + var reporterBridge: ProgressReporterBridge? + var nsProgressBridge: Foundation.Progress? + } +} + +extension ProgressManager.State { + internal func notifyObservers(with observerState: ProgressManager.ObserverState) { + for observer in observers { + observer(observerState) + } + } +} + +@available(FoundationPreview 6.2, *) +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 + if let reporterBridge { + state.interopObservation.reporterBridge = reporterBridge + } + + if let nsProgressBridge { + state.interopObservation.nsProgressBridge = nsProgressBridge + } + } + } + + internal func setInteropChild(interopChild: ProgressManager) { + state.withLock { state in + state.interopChild = interopChild + } + } +} +#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..13bf70f77 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -0,0 +1,496 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +internal import Synchronization + +@available(FoundationPreview 6.2, *) +extension ProgressManager { + + /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> P.Summary where P.Value == Int, P.Summary == Int { + return getUpdatedIntSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> P.Summary where P.Value == Double, P.Summary == Double { + return getUpdatedDoubleSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for the specified 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 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) -> P.Summary where P.Value == String, P.Summary == String { + return getUpdatedStringSummary(property: MetatypeWrapper(property)) + } + + /// Returns the total file count across the progress subtree. + /// + /// - Parameter property: The `TotalFileCount` property type. + /// - Returns: The sum of all total file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.TotalFileCount.Type) -> Int { + return getUpdatedFileCount(type: .total) + } + + /// Returns the completed file count across the progress subtree. + /// + /// - Parameter property: The `CompletedFileCount` property type. + /// - Returns: The sum of all completed file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.CompletedFileCount.Type) -> Int { + return getUpdatedFileCount(type: .completed) + } + + /// Returns the total byte count across the progress subtree. + /// + /// - Parameter property: The `TotalByteCount` property type. + /// - Returns: The sum of all total byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.TotalByteCount.Type) -> UInt64 { + return getUpdatedByteCount(type: .total) + } + + /// Returns the completed byte count across the progress subtree. + /// + /// - Parameter property: The `CompletedByteCount` property type. + /// - Returns: The sum of all completed byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.CompletedByteCount.Type) -> UInt64 { + return getUpdatedByteCount(type: .completed) + } + + /// Returns the average throughput across the progress subtree. + /// + /// - Parameter property: The `Throughput` property type. + /// - Returns: The average throughput across the entire progress subtree, in bytes per second. + /// + /// - Note: The throughput is calculated as the sum of all throughput values divided by the count + /// of progress managers that have throughput data. + public func summary(of property: ProgressManager.Properties.Throughput.Type) -> UInt64 { + let throughput = getUpdatedThroughput() + return throughput.values / UInt64(throughput.count) + } + + /// Returns the maximum estimated time remaining for completion across the progress subtree. + /// + /// - Parameter property: The `EstimatedTimeRemaining` property type. + /// - Returns: The estimated duration until completion for the entire progress subtree. + /// + /// - Note: The estimation is based on current throughput and remaining work. The accuracy + /// depends on the consistency of the processing rate. + public func summary(of property: ProgressManager.Properties.EstimatedTimeRemaining.Type) -> Duration { + return getUpdatedEstimatedTimeRemaining() + } + + /// Returns all file URLs being processed across the progress subtree. + /// + /// - Parameter property: The `FileURL` property type. + /// - Returns: An array containing all file URLs across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL] { + return getUpdatedFileURL() + } + + // MARK: Additional Properties Methods + internal func getProperties( + _ closure: (sending Values) throws(E) -> sending T + ) throws(E) -> sending T { + try state.withLock { state throws(E) -> T in + let values = Values(state: state) + let result = try closure(values) + return result + } + } + + /// Mutates any settable properties that convey information about progress. + public func withProperties( + _ closure: (inout sending Values) throws(E) -> sending T + ) throws(E) -> sending T { + return try state.withLock { (state) throws(E) -> T in + var values = Values(state: state) + // This is done to avoid copy on write later +#if FOUNDATION_FRAMEWORK + state = State( + selfFraction: ProgressFraction(), + 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, + propertiesInt: [:], + propertiesDouble: [:], + propertiesString: [:], + interopObservation: InteropObservation(subprogressBridge: nil), + observers: [] + ) +#else + state = State( + selfFraction: ProgressFraction(), + 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, + propertiesInt: [:], + propertiesDouble: [:], + propertiesString: [:] + ) +#endif + let result = try closure(&values) + if values.fractionalCountDirty { + markSelfDirty(parents: values.state.parents) + } + + if values.totalFileCountDirty { + markSelfDirty(property: Properties.TotalFileCount.self, parents: values.state.parents) + } + + if values.completedFileCountDirty { + markSelfDirty(property: Properties.CompletedFileCount.self, parents: values.state.parents) + } + + if values.totalByteCountDirty { + markSelfDirty(property: Properties.TotalByteCount.self, parents: values.state.parents) + } + + if values.completedByteCountDirty { + markSelfDirty(property: Properties.CompletedByteCount.self, parents: values.state.parents) + } + + if values.throughputDirty { + markSelfDirty(property: Properties.Throughput.self, parents: values.state.parents) + } + + if values.estimatedTimeRemainingDirty { + markSelfDirty(property: Properties.EstimatedTimeRemaining.self, parents: values.state.parents) + } + + if values.fileURLDirty { + markSelfDirty(property: Properties.FileURL.self, parents: values.state.parents) + } + + if values.dirtyPropertiesInt.count > 0 { + for property in values.dirtyPropertiesInt { + markSelfDirty(property: property, parents: values.state.parents) + } + } + + if values.dirtyPropertiesDouble.count > 0 { + for property in values.dirtyPropertiesDouble { + markSelfDirty(property: property, parents: values.state.parents) + } + } + + if values.dirtyPropertiesString.count > 0 { + for property in values.dirtyPropertiesString { + markSelfDirty(property: property, parents: values.state.parents) + } + } +#if FOUNDATION_FRAMEWORK + if let observerState = values.observerState { + if let _ = state.interopObservation.reporterBridge { + notifyObservers(with: observerState) + } + } +#endif + state = values.state + return result + } + } + + /// A container that holds values for properties that specify information on progress. + @dynamicMemberLookup + public struct Values : Sendable { + //TODO: rdar://149225947 Non-escapable conformance + internal var state: State + + internal var fractionalCountDirty = false + internal var totalFileCountDirty = false + internal var completedFileCountDirty = false + internal var totalByteCountDirty = false + internal var completedByteCountDirty = false + internal var throughputDirty = false + internal var estimatedTimeRemainingDirty = false + internal var fileURLDirty = false + internal var dirtyPropertiesInt: [MetatypeWrapper] = [] + internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] + internal var dirtyPropertiesString: [MetatypeWrapper] = [] +#if FOUNDATION_FRAMEWORK + internal var observerState: ObserverState? +#endif + + /// The total units of work. + public var totalCount: Int? { + get { + state.getTotalCount() + } + + set { + guard newValue != state.selfFraction.total else { + return + } + + state.selfFraction.total = newValue + +#if FOUNDATION_FRAMEWORK + interopNotifications() +#endif + + fractionalCountDirty = true + } + } + + /// The completed units of work. + public var completedCount: Int { + mutating get { + state.getCompletedCount() + } + + set { + guard newValue != state.selfFraction.completed else { + return + } + + state.selfFraction.completed = newValue + +#if FOUNDATION_FRAMEWORK + interopNotifications() +#endif + fractionalCountDirty = true + } + } + + /// Gets or sets the total file count property. + /// - Parameter key: A key path to the `TotalFileCount` property type. + public subscript(dynamicMember key: KeyPath) -> Int { + get { + return state.totalFileCount + } + + set { + + guard newValue != state.totalFileCount else { + return + } + + state.totalFileCount = newValue + + totalFileCountDirty = true + } + } + + /// Gets or sets the completed file count property. + /// - Parameter key: A key path to the `CompletedFileCount` property type. + public subscript(dynamicMember key: KeyPath) -> Int { + get { + return state.completedFileCount + } + + set { + + guard newValue != state.completedFileCount else { + return + } + + state.completedFileCount = newValue + + completedFileCountDirty = true + } + } + + /// Gets or sets the total byte count property. + /// - Parameter key: A key path to the `TotalByteCount` property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 { + get { + return state.totalByteCount + } + + set { + guard newValue != state.totalByteCount else { + return + } + + state.totalByteCount = newValue + + totalByteCountDirty = true + } + } + + /// Gets or sets the completed byte count property. + /// - Parameter key: A key path to the `CompletedByteCount` property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 { + get { + return state.completedByteCount + } + + set { + guard newValue != state.completedByteCount else { + return + } + + state.completedByteCount = newValue + + completedByteCountDirty = true + } + } + + /// Gets or sets the throughput property. + /// - Parameter key: A key path to the `Throughput` property type. + public subscript(dynamicMember key: KeyPath) -> UInt64 { + get { + return state.throughput + } + + set { + guard newValue != state.throughput else { + return + } + + state.throughput = newValue + + throughputDirty = true + } + } + + /// Gets or sets the estimated time remaining property. + /// - Parameter key: A key path to the `EstimatedTimeRemaining` property type. + public subscript(dynamicMember key: KeyPath) -> Duration { + get { + return state.estimatedTimeRemaining + } + + set { + guard newValue != state.estimatedTimeRemaining else { + return + } + + state.estimatedTimeRemaining = newValue + + estimatedTimeRemainingDirty = true + } + } + + /// Gets or sets the file URL property. + /// - Parameter key: A key path to the `FileURL` property type. + public subscript(dynamicMember key: KeyPath) -> URL? { + get { + return state.fileURL + } + + set { + guard newValue != state.fileURL else { + return + } + + state.fileURL = newValue + + fileURLDirty = true + } + } + + /// 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 { + return state.propertiesInt[MetatypeWrapper(P.self)] ?? P.defaultValue + } + + set { + guard newValue != state.propertiesInt[MetatypeWrapper(P.self)] else { + return + } + + state.propertiesInt[MetatypeWrapper(P.self)] = newValue + + dirtyPropertiesInt.append(MetatypeWrapper(P.self)) + } + } + + /// 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 { + return state.propertiesDouble[MetatypeWrapper(P.self)] ?? P.defaultValue + } + + set { + guard newValue != state.propertiesDouble[MetatypeWrapper(P.self)] else { + return + } + + state.propertiesDouble[MetatypeWrapper(P.self)] = newValue + + dirtyPropertiesDouble.append(MetatypeWrapper(P.self)) + } + } + + /// Gets or sets custom string properties. + /// + /// This subscript provides read-write access to custom progress properties where both the value + /// and summary types are `String`. If the property has not een 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 { + return state.propertiesString[MetatypeWrapper(P.self)] ?? P.self.defaultValue + } + + set { + guard newValue != state.propertiesString[MetatypeWrapper(P.self)] else { + return + } + + state.propertiesString[MetatypeWrapper(P.self)] = newValue + + dirtyPropertiesString.append(MetatypeWrapper(P.self)) + } + } +#if FOUNDATION_FRAMEWORK + private mutating func interopNotifications() { + state.interopObservation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) + + self.observerState = .fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed) + } +#endif + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift new file mode 100644 index 000000000..e4a4fe59d --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -0,0 +1,261 @@ +//===----------------------------------------------------------------------===// +// +// 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.2, *) +extension ProgressManager { + + /// A type that conveys 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 like completion counts and file counts. + /// + /// Properties use a key-value system where the key uniquely identifies the property type, + /// and values can be aggregated across progress manager hierarchies through reduction and merging operations. + public protocol Property: SendableMetatype { + + /// The type of individual values stored in this property. + /// + /// This associated type represents the data type of individual property values + /// that can be set on progress managers. Must be `Sendable` and `Equatable`. + associatedtype Value: Sendable, Equatable + + /// The type used for aggregated summaries of this property. + /// + /// This associated type represents the data type used when summarizing property values + /// across multiple progress managers in a hierarchy. Must be `Sendable` and `Equatable`. + 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, or as a fallback when summarization fails. + /// + /// - 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 + } + + // Namespace for properties specific to operations reported on + public struct Properties: Sendable { + + /// The total number of files. + public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } + public struct 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 + } + } + + /// The number of completed files. + public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } + public struct 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 + } + } + + /// The total number of bytes. + public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } + public struct 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 + } + } + + /// The number of completed bytes. + public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } + public struct 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 + } + } + + /// The throughput, in bytes per second. + public var throughput: Throughput.Type { Throughput.self } + public struct Throughput: Sendable, Property { + public typealias Value = UInt64 + + public struct AggregateThroughput: Sendable, Equatable { + var values: UInt64 + var count: Int + } + + public typealias Summary = AggregateThroughput + + public static var key: String { return "Foundation.ProgressManager.Properties.Throughput" } + + public static var defaultValue: UInt64 { return 0 } + + public static var defaultSummary: AggregateThroughput { return AggregateThroughput(values: 0, count: 0) } + + public static func reduce(into summary: inout AggregateThroughput, value: UInt64) { + summary = Summary(values: summary.values + value, count: summary.count + 1) + } + + public static func merge(_ summary1: AggregateThroughput, _ summary2: AggregateThroughput) -> AggregateThroughput { + return Summary(values: summary1.values + summary2.values, count: summary1.count + summary2.count) + } + } + + /// The amount of time remaining in the processing of files. + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } + public struct 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) + } + } + + + /// The URL of file being processed. + public var fileURL: FileURL.Type { FileURL.self } + public struct FileURL: Sendable, Property { + + public typealias Value = URL? + + public typealias Summary = [URL] + + public static var key: String { return "Foundation.ProgressManager.Properties.FileURL" } + + public static var defaultValue: URL? { return nil } + + public static var defaultSummary: [URL] { return [] } + + public static func reduce(into summary: inout [URL], value: URL?) { + guard let value else { + return + } + summary.append(value) + } + + public static func merge(_ summary1: [URL], _ summary2: [URL]) -> [URL] { + return summary1 + summary2 + } + + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift new file mode 100644 index 000000000..7c5a79b50 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -0,0 +1,562 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +internal import Synchronization +@available(FoundationPreview 6.2, *) +extension ProgressManager { + + internal enum CountType { + case total + case completed + } + + //MARK: Methods to get updated summary of properties + internal func getUpdatedIntSummary(property: MetatypeWrapper) -> Int { + return state.withLock { state in + + var value: Int = property.defaultSummary + property.reduce(&value, state.propertiesInt[property] ?? property.defaultValue) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if let childPropertyState = childState.childPropertiesInt[property] { + if childPropertyState.isDirty { + if let child = childState.child { + let updatedSummary = child.getUpdatedIntSummary(property: property) + let newChildPropertyState = PropertyStateInt(value: updatedSummary, isDirty: false) + state.children[idx].childPropertiesInt[property] = newChildPropertyState + value = property.merge(value, updatedSummary) + } else { + if let remainingProperties = childState.remainingPropertiesInt { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } else { + value = property.merge(value, childPropertyState.value) + } + } else { + if let child = childState.child { + let childSummary = child.getUpdatedIntSummary(property: property) + let newChildPropertyState = PropertyStateInt(value: childSummary, isDirty: false) + state.children[idx].childPropertiesInt[property] = newChildPropertyState + value = property.merge(value, childSummary) + } else { + // Get value from remainingProperties + if let remainingProperties = childState.remainingPropertiesInt { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } + } + return value + } + } + + internal func getUpdatedDoubleSummary(property: MetatypeWrapper) -> Double { + return state.withLock { state in + + var value: Double = property.defaultSummary + property.reduce(&value, state.propertiesDouble[property] ?? property.defaultValue) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if let childPropertyState = childState.childPropertiesDouble[property] { + if childPropertyState.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedDoubleSummary(property: property) + let newChildPropertyState = PropertyStateDouble(value: updatedSummary, isDirty: false) + state.children[idx].childPropertiesDouble[property] = newChildPropertyState + value = property.merge(value, updatedSummary) + } else { + // Get value from remainingProperties + if let remainingProperties = childState.remainingPropertiesDouble { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } else { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } + } else { + // First fetch of value + if let child = childState.child { + let childSummary = child.getUpdatedDoubleSummary(property: property) + let newChildPropertyState = PropertyStateDouble(value: childSummary, isDirty: false) + state.children[idx].childPropertiesDouble[property] = newChildPropertyState + value = property.merge(value, childSummary) + } else { + // Get value from remainingProperties + if let remainingProperties = childState.remainingPropertiesDouble { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } + } + return value + } + } + + internal func getUpdatedStringSummary(property: MetatypeWrapper) -> String { + return state.withLock { state in + + var value: String = property.defaultSummary + property.reduce(&value, state.propertiesString[property] ?? property.defaultValue) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if let childPropertyState = childState.childPropertiesString[property] { + if childPropertyState.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedStringSummary(property: property) + let newChildPropertyState = PropertyStateString(value: updatedSummary, isDirty: false) + state.children[idx].childPropertiesString[property] = newChildPropertyState + value = property.merge(value, updatedSummary) + } else { + // Get value from remainingProperties + if let remainingProperties = childState.remainingPropertiesString { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } else { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } + } else { + // First fetch of value + if let child = childState.child { + let childSummary = child.getUpdatedStringSummary(property: property) + let newChildPropertyState = PropertyStateString(value: childSummary, isDirty: false) + state.children[idx].childPropertiesString[property] = newChildPropertyState + value = property.merge(value, childSummary) + } else { + // Get value from remainingProperties + if let remainingProperties = childState.remainingPropertiesString { + if let remainingSummary = remainingProperties[property] { + value = property.merge(value, remainingSummary) + } + } + } + } + } + return value + } + } + + internal func getUpdatedFileCount(type: CountType) -> Int { + switch type { + case .total: + return state.withLock { state in + // Get self's totalFileCount as part of summary + var value: Int = 0 + ProgressManager.Properties.TotalFileCount.reduce(into: &value, value: state.totalFileCount) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.totalFileCount.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedFileCount(type: type) + let newTotalFileCountState = PropertyStateInt(value: updatedSummary, isDirty: false) + state.children[idx].totalFileCount = newTotalFileCountState + value = ProgressManager.Properties.TotalFileCount.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.TotalFileCount.merge(value, childState.totalFileCount.value) + } + } + return value + } + case .completed: + return state.withLock { state in + // Get self's completedFileCount as part of summary + var value: Int = 0 + ProgressManager.Properties.CompletedFileCount.reduce(into: &value, value: state.completedFileCount) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.completedFileCount.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedFileCount(type: type) + let newCompletedFileCountState = PropertyStateInt(value: updatedSummary, isDirty: false) + state.children[idx].completedFileCount = newCompletedFileCountState + value = ProgressManager.Properties.CompletedFileCount.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.CompletedFileCount.merge(value, childState.completedFileCount.value) + } + } + return value + } + } + } + + internal func getUpdatedByteCount(type: CountType) -> UInt64 { + switch type { + case .total: + return state.withLock { state in + // Get self's totalByteCount as part of summary + var value: UInt64 = 0 + ProgressManager.Properties.TotalByteCount.reduce(into: &value, value: state.totalByteCount) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.totalByteCount.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedByteCount(type: type) + let newTotalByteCountState = PropertyStateUInt64(value: updatedSummary, isDirty: false) + state.children[idx].totalByteCount = newTotalByteCountState + value = ProgressManager.Properties.TotalByteCount.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.TotalByteCount.merge(value, childState.totalByteCount.value) + } + } + return value + } + case .completed: + return state.withLock { state in + // Get self's completedByteCount as part of summary + var value: UInt64 = 0 + ProgressManager.Properties.CompletedByteCount.reduce(into: &value, value: state.completedByteCount) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.completedByteCount.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedByteCount(type: type) + let newCompletedByteCountState = PropertyStateUInt64(value: updatedSummary, isDirty: false) + state.children[idx].completedByteCount = newCompletedByteCountState + value = ProgressManager.Properties.CompletedByteCount.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.CompletedByteCount.merge(value, childState.completedByteCount.value) + } + } + return value + } + } + } + + internal func getUpdatedThroughput() -> ProgressManager.Properties.Throughput.AggregateThroughput { + return state.withLock { state in + // Get self's throughput as part of summary + var value: ProgressManager.Properties.Throughput.AggregateThroughput = ProgressManager.Properties.Throughput.defaultSummary + ProgressManager.Properties.Throughput.reduce(into: &value, value: state.throughput) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.throughput.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedThroughput() + let newThroughputState = PropertyStateThroughput(value: updatedSummary, isDirty: false) + state.children[idx].throughput = newThroughputState + value = ProgressManager.Properties.Throughput.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.Throughput.merge(value, childState.throughput.value) + } + } + return value + } + } + + internal func getUpdatedEstimatedTimeRemaining() -> Duration { + return state.withLock { state in + // Get self's estimatedTimeRemaining as part of summary + var value: Duration = Duration.seconds(0) + ProgressManager.Properties.EstimatedTimeRemaining.reduce(into: &value, value: state.estimatedTimeRemaining) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.estimatedTimeRemaining.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedEstimatedTimeRemaining() + let newDurationState = PropertyStateDuration(value: updatedSummary, isDirty: false) + state.children[idx].estimatedTimeRemaining = newDurationState + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, childState.estimatedTimeRemaining.value) + } + } + return value + } + } + + internal func getUpdatedFileURL() -> [URL] { + return state.withLock { state in + // Get self's estimatedTimeRemaining as part of summary + var value: [URL] = ProgressManager.Properties.FileURL.defaultSummary + ProgressManager.Properties.FileURL.reduce(into: &value, value: state.fileURL) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if childState.fileURL.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedFileURL() + let newFileURL = PropertyStateURL(value: updatedSummary, isDirty: false) + state.children[idx].fileURL = newFileURL + value = ProgressManager.Properties.FileURL.merge(value, updatedSummary) + } + } else { + // Merge non-dirty, updated value + value = ProgressManager.Properties.FileURL.merge(value, childState.fileURL.value) + } + } + return value + } + } + + //MARK: Methods to set dirty bit recursively + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedFileCount.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.TotalByteCount.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.CompletedByteCount.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.Throughput.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markSelfDirty(property: ProgressManager.Properties.FileURL.Type, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + let parents = state.withLock { state in + state.children[position].childPropertiesInt[property]?.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + let parents = state.withLock { state in + state.children[position].childPropertiesDouble[property]?.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + let parents = state.withLock { state in + state.children[position].childPropertiesString[property]?.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].totalFileCount.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedFileCount.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].completedFileCount.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.TotalByteCount.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].totalByteCount.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.CompletedByteCount.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].completedByteCount.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.Throughput.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].throughput.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.EstimatedTimeRemaining.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].estimatedTimeRemaining.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + internal func markChildDirty(property: ProgressManager.Properties.FileURL.Type, at position: Int) { + let parents = state.withLock { state in + state.children[position].fileURL.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } + + //MARK: Methods to preserve values of properties upon deinit + internal func setChildRemainingPropertiesInt(_ properties: [MetatypeWrapper: Int], at position: Int) { + state.withLock { state in + state.children[position].remainingPropertiesInt = properties + } + } + + internal func setChildRemainingPropertiesDouble(_ properties: [MetatypeWrapper: Double], at position: Int) { + state.withLock { state in + state.children[position].remainingPropertiesDouble = properties + } + } + + internal func setChildRemainingPropertiesString(_ properties: [MetatypeWrapper: String], at position: Int) { + state.withLock { state in + state.children[position].remainingPropertiesString = properties + } + } + + internal func setChildTotalFileCount(value: Int, at position: Int) { + state.withLock { state in + state.children[position].totalFileCount = PropertyStateInt(value: value, isDirty: false) + } + } + + internal func setChildCompletedFileCount(value: Int, at position: Int) { + state.withLock { state in + state.children[position].completedFileCount = PropertyStateInt(value: value, isDirty: false) + } + } + + internal func setChildTotalByteCount(value: UInt64, at position: Int) { + state.withLock { state in + state.children[position].totalByteCount = PropertyStateUInt64(value: value, isDirty: false) + } + } + + internal func setChildCompletedByteCount(value: UInt64, at position: Int) { + state.withLock { state in + state.children[position].completedByteCount = PropertyStateUInt64(value: value, isDirty: false) + } + } + + internal func setChildThroughput(value: ProgressManager.Properties.Throughput.AggregateThroughput, at position: Int) { + state.withLock { state in + state.children[position].throughput = PropertyStateThroughput(value: value, isDirty: false) + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift new file mode 100644 index 000000000..2bd9a581a --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -0,0 +1,181 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +internal import Synchronization + +@available(FoundationPreview 6.2, *) +extension ProgressManager { + + internal struct MetatypeWrapper: Hashable, Equatable, Sendable { + + let reduce: @Sendable (inout T, T) -> () + let merge: @Sendable (T, T) -> T + + let defaultValue: T + let defaultSummary: T + + let key: String + + init(_ argument: P.Type) where P.Value == T, P.Summary == T { + reduce = P.reduce + merge = P.merge + 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 PropertyStateThroughput { + var value: ProgressManager.Properties.Throughput.AggregateThroughput + 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 ChildState { + weak var child: ProgressManager? + var remainingPropertiesInt: [MetatypeWrapper: Int]? + var remainingPropertiesDouble: [MetatypeWrapper: Double]? + var remainingPropertiesString: [MetatypeWrapper: String]? + var portionOfTotal: Int + var childFraction: ProgressFraction + var isDirty: Bool + var totalFileCount: PropertyStateInt + var completedFileCount: PropertyStateInt + var totalByteCount: PropertyStateUInt64 + var completedByteCount: PropertyStateUInt64 + var throughput: PropertyStateThroughput + var estimatedTimeRemaining: PropertyStateDuration + var fileURL: PropertyStateURL + var childPropertiesInt: [MetatypeWrapper: PropertyStateInt] + var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] + var childPropertiesString: [MetatypeWrapper: PropertyStateString] + } + + internal struct ParentState { + var parent: ProgressManager + var positionInParent: Int + } + + internal struct State { + var selfFraction: ProgressFraction + var overallFraction: ProgressFraction { + var overallFraction = selfFraction + for child in children { + if !child.childFraction.isFinished { + let multiplier = ProgressFraction(completed: child.portionOfTotal, total: selfFraction.total) + if let additionalFraction = multiplier * child.childFraction { + overallFraction = overallFraction + additionalFraction + } + } + } + return overallFraction + } + var children: [ChildState] + var parents: [ParentState] + var totalFileCount: Int + var completedFileCount: Int + var totalByteCount: UInt64 + var completedByteCount: UInt64 + var throughput: UInt64 + var estimatedTimeRemaining: Duration + var fileURL: URL? + var propertiesInt: [MetatypeWrapper: Int] + var propertiesDouble: [MetatypeWrapper: Double] + var propertiesString: [MetatypeWrapper: String] +#if FOUNDATION_FRAMEWORK + var interopChild: ProgressManager? + var interopObservation: InteropObservation + var observers: [@Sendable (ObserverState) -> Void] +#endif + + /// Returns nil if `self` was instantiated without total units; + /// returns a `Int` value otherwise. + internal func getTotalCount() -> Int? { +#if FOUNDATION_FRAMEWORK + if let interopChild = interopChild { + return interopChild.totalCount + } +#endif + return selfFraction.total + } + + /// Returns 0 if `self` has `nil` total units; + /// returns a `Int` value otherwise. + internal mutating func getCompletedCount() -> Int { +#if FOUNDATION_FRAMEWORK + if let interopChild = interopChild { + return interopChild.completedCount + } +#endif + + updateChildrenProgressFraction() + + return selfFraction.completed + } + + internal mutating func updateChildrenProgressFraction() { + guard !children.isEmpty else { + return + } + for (idx, childState) in children.enumerated() { + if childState.isDirty { + if let child = childState.child { + let updatedProgressFraction = child.getUpdatedProgressFraction() + children[idx].childFraction = updatedProgressFraction + if updatedProgressFraction.isFinished { + selfFraction.completed += children[idx].portionOfTotal + } + } else { + selfFraction.completed += children[idx].portionOfTotal + } + children[idx].isDirty = false + } + } + } + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift new file mode 100644 index 000000000..9cd23bedb --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -0,0 +1,413 @@ +//===----------------------------------------------------------------------===// +// +// 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 +internal import Synchronization + +#if canImport(CollectionsInternal) +internal import CollectionsInternal +#elseif canImport(OrderedCollections) +internal import OrderedCollections +#elseif canImport(_FoundationCollections) +internal import _FoundationCollections +#endif + +@available(FoundationPreview 6.2, *) +/// An object that conveys ongoing progress to the user for a specified task. +@Observable public final class ProgressManager: Sendable { + + internal let state: Mutex + + /// The total units of work. + public var totalCount: Int? { + _$observationRegistrar.access(self, keyPath: \.totalCount) + return state.withLock { state in + state.getTotalCount() + } + } + + /// The completed units of work. + /// If `self` is indeterminate, the value will be 0. + public var completedCount: Int { + _$observationRegistrar.access(self, keyPath: \.completedCount) + return state.withLock { state in + state.getCompletedCount() + } + } + + /// 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 { + _$observationRegistrar.access(self, keyPath: \.fractionCompleted) + return state.withLock { state in +#if FOUNDATION_FRAMEWORK + if let interopChild = state.interopChild { + return interopChild.fractionCompleted + } +#endif + + state.updateChildrenProgressFraction() + + return state.overallFraction.fractionCompleted + } + } + + /// The state of initialization of `totalCount`. + /// If `totalCount` is `nil`, the value will be `true`. + public var isIndeterminate: Bool { + _$observationRegistrar.access(self, keyPath: \.isIndeterminate) + return state.withLock { state in +#if FOUNDATION_FRAMEWORK + if let interopChild = state.interopChild { + return interopChild.isIndeterminate + } +#endif + return state.selfFraction.isIndeterminate + } + } + + /// The state of completion of work. + /// If `completedCount` >= `totalCount`, the value will be `true`. + public var isFinished: Bool { + _$observationRegistrar.access(self, keyPath: \.isFinished) + return state.withLock { state in +#if FOUNDATION_FRAMEWORK + if let interopChild = state.interopChild { + return interopChild.isIndeterminate + } +#endif + return state.selfFraction.isFinished + } + } + + /// 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, + fileURL: ProgressManager.Properties.FileURL.defaultValue, + propertiesInt: [:], + propertiesDouble: [:], + propertiesString: [:], + interopChild: nil, + interopObservation: InteropObservation(subprogressBridge: subprogressBridge), + observers: [] + ) + 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, + fileURL: ProgressManager.Properties.FileURL.defaultValue, + propertiesInt: [:], + propertiesDouble: [:], + propertiesString: [:] + ) + 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: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + /// - Returns: A `Subprogress` instance. + public func subprogress(assigningCount portionOfParent: Int) -> Subprogress { + precondition(portionOfParent > 0, "Giving out zero units is not a valid operation.") + let subprogress = Subprogress(parent: self, portionOfParent: portionOfParent) + return subprogress + } + + /// Adds a `ProgressReporter` as a child, with its progress representing a portion of `self`'s progress. + /// - Parameters: + /// - reporter: A `ProgressReporter` instance. + /// - count: Units, which is a portion of `totalCount`delegated to an instance of `Subprogress`. + 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(child: actualManager, portion: count, childFraction: actualManager.getProgressFraction()) + actualManager.addParent(parent: self, positionInParent: position) + } + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + let parents: [ParentState]? = state.withLock { state in + guard state.selfFraction.completed != (state.selfFraction.completed + count) else { + return nil + } + + state.selfFraction.completed += count + +#if FOUNDATION_FRAMEWORK + state.interopObservation.subprogressBridge?.manager.notifyObservers( + with: .fractionUpdated( + totalCount: state.selfFraction.total ?? 0, + completedCount: state.selfFraction.completed + ) + ) + + if let _ = state.interopObservation.reporterBridge { + state.notifyObservers( + with: .fractionUpdated( + totalCount: state.selfFraction.total ?? 0, + completedCount: state.selfFraction.completed + ) + ) + } +#endif + + return state.parents + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + + //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: [ParentState]) { + _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { + if parents.count > 0 { + for parentState in parents { + parentState.parent.markChildDirty(at: parentState.positionInParent) + } + } + } + } + + private func markChildDirty(at position: Int) { + let parents: [ParentState]? = state.withLock { state in + guard !state.children[position].isDirty else { + return nil + } + state.children[position].isDirty = true + return state.parents + } + if let parents = parents { + markSelfDirty(parents: parents) + } + } + + internal func getUpdatedProgressFraction() -> ProgressFraction { + return state.withLock { state in + state.updateChildrenProgressFraction() + return state.overallFraction + } + } + + //MARK: Parent - Child Relationship Methods + internal func addChild(child: ProgressManager, portion: Int, childFraction: ProgressFraction) -> Int { + let (index, parents) = state.withLock { state in + let childState = ChildState(child: child, + remainingPropertiesInt: nil, + portionOfTotal: portion, + childFraction: childFraction, + isDirty: true, + totalFileCount: PropertyStateInt(value: ProgressManager.Properties.TotalFileCount.defaultSummary, isDirty: false), + completedFileCount: PropertyStateInt(value: ProgressManager.Properties.CompletedFileCount.defaultSummary, isDirty: false), + totalByteCount: PropertyStateUInt64(value: ProgressManager.Properties.TotalByteCount.defaultSummary, isDirty: false), + completedByteCount: PropertyStateUInt64(value: ProgressManager.Properties.CompletedByteCount.defaultSummary, isDirty: false), + throughput: PropertyStateThroughput(value: ProgressManager.Properties.Throughput.defaultSummary, isDirty: false), + estimatedTimeRemaining: PropertyStateDuration(value: ProgressManager.Properties.EstimatedTimeRemaining.defaultSummary, isDirty: false), + fileURL: PropertyStateURL(value: ProgressManager.Properties.FileURL.defaultSummary, isDirty: false), + childPropertiesInt: [:], + childPropertiesDouble: [:], + childPropertiesString: [:]) + state.children.append(childState) + 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(parent: ProgressManager, positionInParent: Int) { + state.withLock { state in + let parentState = ParentState(parent: parent, positionInParent: positionInParent) + state.parents.append(parentState) + } + } + + // MARK: Cycle Detection Methods + internal func isCycle(reporter: ProgressReporter, visited: Set = []) -> Bool { + if reporter.manager === self { + return true + } + + let updatedVisited = visited.union([self]) + + return state.withLock { state in + for parentState in state.parents { + if !updatedVisited.contains(parentState.parent) { + if (parentState.parent.isCycle(reporter: reporter, visited: updatedVisited)) { + return true + } + } + } + return false + } + } + + internal func isCycleInterop(reporter: ProgressReporter, visited: Set = []) -> Bool { + return state.withLock { state in + for parentState in state.parents { + if !visited.contains(parentState.parent) { + if (parentState.parent.isCycle(reporter: reporter, visited: visited)) { + return true + } + } + } + return false + } + } + + deinit { + if !isFinished { + self.withProperties { properties in + if let totalCount = properties.totalCount { + properties.completedCount = totalCount + } + } + } + + let (propertiesInt, propertiesDouble, propertiesString, parents) = state.withLock { state in + return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.parents) + } + + var finalSummaryInt: [MetatypeWrapper: Int] = [:] + for property in propertiesInt.keys { + let updatedSummary = self.getUpdatedIntSummary(property: property) + finalSummaryInt[property] = updatedSummary + } + + var finalSummaryDouble: [MetatypeWrapper: Double] = [:] + for property in propertiesDouble.keys { + let updatedSummary = self.getUpdatedDoubleSummary(property: property) + finalSummaryDouble[property] = updatedSummary + } + + var finalSummaryString: [MetatypeWrapper: String] = [:] + for property in propertiesString.keys { + let updatedSummary = self.getUpdatedStringSummary(property: property) + finalSummaryString[property] = updatedSummary + } + + + for parentState in parents { + parentState.parent.setChildRemainingPropertiesInt(finalSummaryInt, at: parentState.positionInParent) + parentState.parent.setChildRemainingPropertiesDouble(finalSummaryDouble, at: parentState.positionInParent) + parentState.parent.setChildRemainingPropertiesString(finalSummaryString, at: parentState.positionInParent) + parentState.parent.setChildTotalFileCount(value: self.getUpdatedFileCount(type: .total), at: parentState.positionInParent) + parentState.parent.setChildCompletedFileCount(value: self.getUpdatedFileCount(type: .completed), at: parentState.positionInParent) + parentState.parent.setChildTotalByteCount(value: self.getUpdatedByteCount(type: .total), at: parentState.positionInParent) + parentState.parent.setChildCompletedByteCount(value: self.getUpdatedByteCount(type: .completed), at: parentState.positionInParent) + parentState.parent.setChildThroughput(value: self.getUpdatedThroughput(), at: parentState.positionInParent) + } + } +} + +@available(FoundationPreview 6.2, *) +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.2, *) +extension ProgressManager: CustomStringConvertible, CustomDebugStringConvertible { + 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)) + """ + } + + 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..3a3195b6b --- /dev/null +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -0,0 +1,178 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@available(FoundationPreview 6.2, *) +/// 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. +@Observable public final class ProgressReporter: Sendable, CustomStringConvertible, CustomDebugStringConvertible { + + /// 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 + } + + 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)) + """ + } + + public var debugDescription: String { + return self.description + } + + /// Reads properties that convey additional information about progress. + public func withProperties( + _ closure: (sending ProgressManager.Values) throws(E) -> sending T + ) throws(E) -> T { + return try manager.getProperties(closure) + } + + /// 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 double 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 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 { + manager.summary(of: property) + } + + /// Returns the total file count across the progress subtree. + /// + /// - Parameter property: The `TotalFileCount` property type. + /// - Returns: The sum of all total file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.TotalFileCount.Type) -> Int { + return manager.summary(of: property) + } + + /// Returns the completed file count across the progress subtree. + /// + /// - Parameter property: The `CompletedFileCount` property type. + /// - Returns: The sum of all completed file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.CompletedFileCount.Type) -> Int { + manager.summary(of: property) + } + + /// Returns the total byte count across the progress subtree. + /// + /// - Parameter property: The `TotalByteCount` property type. + /// - Returns: The sum of all total byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.TotalByteCount.Type) -> UInt64 { + manager.summary(of: property) + } + + /// Returns the completed byte count across the progress subtree. + /// + /// - Parameter property: The `CompletedByteCount` property type. + /// - Returns: The sum of all completed byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.CompletedByteCount.Type) -> UInt64 { + manager.summary(of: property) + } + + /// Returns the average throughput across the progress subtree. + /// + /// - Parameter property: The `Throughput` property type. + /// - Returns: The average throughput across the entire progress subtree, in bytes per second. + public func summary(of property: ProgressManager.Properties.Throughput.Type) -> UInt64 { + manager.summary(of: property) + } + + /// Returns the maximum estimated time remaining for completion across the progress subtree. + /// + /// - Parameter property: The `EstimatedTimeRemaining` property type. + /// - Returns: The estimated duration until completion for the entire progress subtree. + public func summary(of property: ProgressManager.Properties.EstimatedTimeRemaining.Type) -> Duration { + manager.summary(of: property) + } + + /// Returns all file URLs being processed across the progress subtree. + /// + /// - Parameter property: The `FileURL` property type. + /// - Returns: An array containing all file URLs across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL] { + manager.summary(of: property) + } + + internal let manager: ProgressManager + + internal init(manager: ProgressManager) { + self.manager = manager + } +} diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift new file mode 100644 index 000000000..bc4f02e79 --- /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 +// +//===----------------------------------------------------------------------===// + +@available(FoundationPreview 6.2, *) +/// Subprogress is a nested ~Copyable struct 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`manager(totalCount:)` on a Subprogress. +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressManager + internal var portionOfParent: Int + internal var isInitializedToProgressReporter: Bool +#if FOUNDATION_FRAMEWORK + internal var subprogressBridge: SubprogressBridge? +#endif + +#if FOUNDATION_FRAMEWORK + internal init(parent: ProgressManager, portionOfParent: Int, subprogressBridge: SubprogressBridge? = nil) { + self.parent = parent + self.portionOfParent = portionOfParent + self.isInitializedToProgressReporter = false + self.subprogressBridge = subprogressBridge + } +#else + internal init(parent: ProgressManager, portionOfParent: Int) { + self.parent = parent + self.portionOfParent = portionOfParent + 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(interopChild: childManager) + return childManager + } +#else + let childManager = ProgressManager( + total: totalCount, + completed: nil + ) +#endif + + let position = parent.addChild( + child: childManager, + portion: portionOfParent, + childFraction: childManager.getProgressFraction() + ) + childManager.addParent( + parent: parent, + positionInParent: position + ) + + return childManager + } + + deinit { + if !self.isInitializedToProgressReporter { + parent.complete(count: portionOfParent) + } + } +} 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..8992e51fe --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -0,0 +1,734 @@ +//===----------------------------------------------------------------------===// +// +// 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.withProperties { properties in + properties.totalFileCount = 100 + } + + #expect(manager.withProperties(\.totalFileCount) == 100) + + manager.complete(count: 100) + #expect(manager.fractionCompleted == 1.0) + #expect(manager.isFinished == true) + + manager.withProperties { properties in + properties.completedFileCount = 100 + } + #expect(manager.withProperties(\.completedFileCount) == 100) + #expect(manager.withProperties(\.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.withProperties(\.totalFileCount) == 0) + #expect(fileProgressManager.withProperties(\.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.withProperties { properties in + properties.totalFileCount = 10 + properties.completedFileCount = 0 + } + manager1.complete(count: 10) + + #expect(overall.fractionCompleted == 0.5) + + #expect(overall.withProperties(\.totalFileCount) == 0) + #expect(manager1.withProperties(\.totalFileCount) == 10) + #expect(manager1.withProperties(\.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.withProperties { properties in + properties.totalFileCount = 11 + properties.completedFileCount = 0 + } + + let progress2 = overall.subprogress(assigningCount: 1) + let manager2 = progress2.start(totalCount: 10) + + manager2.withProperties { properties in + properties.totalFileCount = 9 + properties.completedFileCount = 0 + } + + #expect(overall.fractionCompleted == 0.0) + #expect(overall.withProperties(\.totalFileCount) == 0) + #expect(overall.withProperties(\.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.withProperties { properties in + properties.completedFileCount = 1 + } + + manager2.withProperties { properties in + properties.completedFileCount = 1 + } + + #expect(overall.withProperties(\.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.withProperties { properties in + properties.totalFileCount += 10 + } + #expect(childManager1.withProperties(\.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.withProperties { properties in + properties.totalFileCount += 10 + } + #expect(childManager2.withProperties(\.totalFileCount) == 10) + + // Tests that totalFileCount propagates to root level + #expect(overall.withProperties(\.totalFileCount) == 0) + let summaryTotalFile = overall.summary(of: ProgressManager.Properties.TotalFileCount.self) + #expect(summaryTotalFile == 20) + + manager1.withProperties { properties in + properties.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.withProperties { properties in + properties.totalByteCount = 300000 + + properties.completedCount += 1 + properties.completedByteCount += 100000 + + properties.completedCount += 1 + properties.completedByteCount += 100000 + + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.totalByteCount = 200000 + properties.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.withProperties { properties in + properties.totalByteCount = 2000 + properties.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.withProperties { properties in + properties.totalByteCount = 500000 + properties.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.withProperties { properties in + properties.totalByteCount = 100000 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.throughput += 1000 + + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.throughput = 1000 + } + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 1500) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.complete(count: 1) + manager.withProperties { properties in + properties.throughput = 1000 + properties.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.withProperties { properties in + properties.throughput = 1000 + } + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 1500) + } + + @Test func threeLevelManager() async throws { + let manager = ProgressManager(totalCount: 2) + manager.complete(count: 1) + + manager.withProperties { properties in + properties.throughput = 1000 + } + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == Int64(4000 / 3)) + } +} + +@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.withProperties { properties in + properties.completedCount = 1 + properties.estimatedTimeRemaining = Duration.seconds(3000) + + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.estimatedTimeRemaining = Duration.seconds(200) + } + + let child = manager.subprogress(assigningCount: 1).start(totalCount: 2) + child.withProperties { properties in + properties.completedCount = 1 + properties.estimatedTimeRemaining = Duration.seconds(80000) + } + + #expect(manager.fractionCompleted == 0.75) + #expect(manager.summary(of: ProgressManager.Properties.EstimatedTimeRemaining.self) == Duration.seconds(80000)) + } + +} + +@Suite("Progress Manager File URL Properties", .tags(.progressManager)) struct ProgressManagerFileURLTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.fileURL = URL(string: "https://www.kittens.com") + } + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.kittens.com")]) + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.fileURL = URL(string: "https://www.cats.com") + } + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.cats.com")]) + } + + @Test func twoLevelManagerWithFinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.fileURL = URL(string: "https://www.cats.com") + } + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.cats.com")]) + } + + @Test func twoLevelManagerWithUnfinishedChild() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.fileURL = URL(string: "https://www.cats.com") + } + + let childManager = manager.subprogress(assigningCount: 1).start(totalCount: 2) + + childManager.withProperties { properties in + properties.completedCount = 1 + properties.fileURL = URL(string: "https://www.kittens.com") + } + + #expect(manager.fractionCompleted == 0.75) + #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.cats.com"), URL(string: "https://www.kittens.com")]) + } +} + +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 "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 + } + } +} + +@Suite("Progress Manager Int Properties", .tags(.progressManager)) struct ProgressManagerIntPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.counter += 10 + + properties.completedCount += 1 + properties.counter += 10 + + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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 justADouble: JustADouble.Type { JustADouble.self } + struct JustADouble: Sendable, ProgressManager.Property { + + typealias Value = Double + + typealias Summary = Double + + static var key: String { return "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 + } + } +} + +@Suite("Progress Manager Double Properties", .tags(.progressManager)) struct ProgressManagerDoublePropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async throws { + let manager = subprogress.start(totalCount: 3) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.justADouble += 10.0 + + properties.completedCount += 1 + properties.justADouble += 10.0 + + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.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 fileName: FileName.Type { FileName.self } + struct FileName: 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 += value + } + + static func merge(_ summary1: String, _ summary2: String) -> String { + return summary1 + ", " + summary2 + } + } +} + + +@Suite("Progress Manager String Properties", .tags(.progressManager)) struct ProgressManagerStringPropertiesTests { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.fileName = "Melon.jpg" + } + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Melon.jpg") + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.fileName = "Cherry.jpg" + } + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Cherry.jpg, Melon.jpg") + } + + @Test func discreteManager() async throws { + let manager = ProgressManager(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount += 1 + properties.fileName = "Grape.jpg" + } + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.withProperties { $0.fileName } == "Grape.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Grape.jpg") + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.fileName = "Watermelon.jpg" + } + + await doSomething(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Watermelon.jpg, Melon.jpg") + } + + @Test func threeLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.fileName = "Watermelon.jpg" + } + + await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Watermelon.jpg, Cherry.jpg, Melon.jpg") + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift new file mode 100644 index 000000000..650ea9751 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -0,0 +1,348 @@ +//===----------------------------------------------------------------------===// +// +// 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.withProperties { p in + p.totalCount = nil + p.completedCount += 1 + } + #expect(overall.completedCount == 6) + #expect(overall.totalCount == nil) + #expect(overall.fractionCompleted == 0.0) + #expect(overall.isIndeterminate == true) + #expect(overall.isFinished == false) + + overall.withProperties { p in + p.totalCount = 12 + p.completedCount += 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.withProperties { p in + p.totalCount = 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.withProperties { properties in + properties.totalCount = nil + } + + #expect(overall.fractionCompleted == 0.0) + #expect(childManager.isIndeterminate == true) + #expect(childManager.completedCount == 2) + + childManager.withProperties { properties in + properties.totalCount = 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.withProperties { p in + p.totalCount = 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.withProperties { properties in + properties.totalFileCount = 10 + } + + #expect(overall.fractionCompleted == 0.5) + // Parent is expected to get totalFileCount from one of the children with a totalFileCount + #expect(overall.withProperties(\.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) + } + } + } + + func makeUnfinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 3) + manager.complete(count: 2) + #expect(manager.fractionCompleted == Double(2) / Double(3)) + } + + @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) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift new file mode 100644 index 000000000..0bcc40784 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressReporterTests.swift @@ -0,0 +1,154 @@ +//===----------------------------------------------------------------------===// +// +// 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.withProperties { properties in + properties.totalFileCount + } + #expect(fileCount == 0) + + manager.withProperties { properties in + properties.totalFileCount = 6 + } + #expect(reporter.withProperties(\.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.withProperties { properties in + properties.totalCount = 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.withProperties { properties in + properties.totalCount = 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 959dbec558c0d8b9992a1e37a52fbc40793f958e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 13 Aug 2025 13:28:23 -0700 Subject: [PATCH 02/24] move interopChild checks into State --- .../ProgressManager+State.swift | 54 +++++++++++++++++++ .../ProgressManager/ProgressManager.swift | 44 ++------------- 2 files changed, 58 insertions(+), 40 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 2bd9a581a..287c0b639 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -158,6 +158,38 @@ extension ProgressManager { return selfFraction.completed } + internal mutating func getFractionCompleted() -> Double { +#if FOUNDATION_FRAMEWORK + // change name later to interopMirror + if let interopChild = interopChild { + return interopChild.fractionCompleted + } +#endif + + updateChildrenProgressFraction() + + return overallFraction.fractionCompleted + + } + + internal func getIsIndeterminate() -> Bool { +#if FOUNDATION_FRAMEWORK + if let interopChild = interopChild { + return interopChild.isIndeterminate + } +#endif + return selfFraction.isIndeterminate + } + + internal func getIsFinished() -> Bool { +#if FOUNDATION_FRAMEWORK + if let interopChild = interopChild { + return interopChild.isIndeterminate + } +#endif + return selfFraction.isFinished + } + internal mutating func updateChildrenProgressFraction() { guard !children.isEmpty else { return @@ -177,5 +209,27 @@ extension ProgressManager { } } } + + internal mutating func complete(by count: Int) { + selfFraction.completed += count + +#if FOUNDATION_FRAMEWORK + interopObservation.subprogressBridge?.manager.notifyObservers( + with: .fractionUpdated( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + + if let _ = interopObservation.reporterBridge { + notifyObservers( + with: .fractionUpdated( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + } +#endif + } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 9cd23bedb..09f86239e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -50,15 +50,7 @@ internal import _FoundationCollections public var fractionCompleted: Double { _$observationRegistrar.access(self, keyPath: \.fractionCompleted) return state.withLock { state in -#if FOUNDATION_FRAMEWORK - if let interopChild = state.interopChild { - return interopChild.fractionCompleted - } -#endif - - state.updateChildrenProgressFraction() - - return state.overallFraction.fractionCompleted + state.getFractionCompleted() } } @@ -67,12 +59,7 @@ internal import _FoundationCollections public var isIndeterminate: Bool { _$observationRegistrar.access(self, keyPath: \.isIndeterminate) return state.withLock { state in -#if FOUNDATION_FRAMEWORK - if let interopChild = state.interopChild { - return interopChild.isIndeterminate - } -#endif - return state.selfFraction.isIndeterminate + state.getIsIndeterminate() } } @@ -81,12 +68,7 @@ internal import _FoundationCollections public var isFinished: Bool { _$observationRegistrar.access(self, keyPath: \.isFinished) return state.withLock { state in -#if FOUNDATION_FRAMEWORK - if let interopChild = state.interopChild { - return interopChild.isIndeterminate - } -#endif - return state.selfFraction.isFinished + state.getIsFinished() } } @@ -191,25 +173,7 @@ internal import _FoundationCollections return nil } - state.selfFraction.completed += count - -#if FOUNDATION_FRAMEWORK - state.interopObservation.subprogressBridge?.manager.notifyObservers( - with: .fractionUpdated( - totalCount: state.selfFraction.total ?? 0, - completedCount: state.selfFraction.completed - ) - ) - - if let _ = state.interopObservation.reporterBridge { - state.notifyObservers( - with: .fractionUpdated( - totalCount: state.selfFraction.total ?? 0, - completedCount: state.selfFraction.completed - ) - ) - } -#endif + state.complete(by: count) return state.parents } From 09f72628893a9b07630e43debd94bcf56e073af7 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 13 Aug 2025 14:35:20 -0700 Subject: [PATCH 03/24] change interop variables into an enum --- .../ProgressManager+Interop.swift | 15 ++- ...ProgressManager+Properties+Accessors.swift | 25 +++-- .../ProgressManager+State.swift | 98 +++++++++++++------ .../ProgressManager/ProgressManager.swift | 5 +- .../ProgressManager/Subprogress.swift | 2 +- 5 files changed, 99 insertions(+), 46 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index b76acbc64..d87b602d2 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -220,6 +220,11 @@ extension ProgressManager { var reporterBridge: ProgressReporterBridge? var nsProgressBridge: Foundation.Progress? } + + internal enum InteropType { + case interopMirror(ProgressManager) + case observation(InteropObservation) + } } extension ProgressManager.State { @@ -252,18 +257,20 @@ extension ProgressManager { internal func addBridge(reporterBridge: ProgressReporterBridge? = nil, nsProgressBridge: Foundation.Progress? = nil) { state.withLock { state in if let reporterBridge { - state.interopObservation.reporterBridge = reporterBridge + let interopObservation = InteropObservation(subprogressBridge: nil, reporterBridge: reporterBridge) + state.interopType = .observation(interopObservation) } if let nsProgressBridge { - state.interopObservation.nsProgressBridge = nsProgressBridge + let interopObservation = InteropObservation(subprogressBridge: nil, nsProgressBridge: nsProgressBridge) + state.interopType = .observation(interopObservation) } } } - internal func setInteropChild(interopChild: ProgressManager) { + internal func setInteropChild(interopMirror: ProgressManager) { state.withLock { state in - state.interopChild = interopChild + state.interopType = .interopMirror(interopMirror) } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index 13bf70f77..fad00269f 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -146,8 +146,8 @@ extension ProgressManager { propertiesInt: [:], propertiesDouble: [:], propertiesString: [:], - interopObservation: InteropObservation(subprogressBridge: nil), - observers: [] + observers: [], + interopType: nil ) #else state = State( @@ -217,8 +217,15 @@ extension ProgressManager { } #if FOUNDATION_FRAMEWORK if let observerState = values.observerState { - if let _ = state.interopObservation.reporterBridge { - notifyObservers(with: observerState) + switch state.interopType { + case .interopMirror: + break + case .observation(let interopObservation): + if let _ = interopObservation.reporterBridge { + notifyObservers(with: observerState) + } + case .none: + break } } #endif @@ -487,8 +494,14 @@ extension ProgressManager { } #if FOUNDATION_FRAMEWORK private mutating func interopNotifications() { - state.interopObservation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) - + switch state.interopType { + case .interopMirror(let mirror): + break + case .observation(let interopObservation): + interopObservation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) + case .none: + break + } self.observerState = .fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed) } #endif diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 287c0b639..3a724e286 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -128,66 +128,93 @@ extension ProgressManager { var propertiesDouble: [MetatypeWrapper: Double] var propertiesString: [MetatypeWrapper: String] #if FOUNDATION_FRAMEWORK - var interopChild: ProgressManager? - var interopObservation: InteropObservation var observers: [@Sendable (ObserverState) -> Void] + var interopType: InteropType? #endif /// Returns nil if `self` was instantiated without total units; /// returns a `Int` value otherwise. internal func getTotalCount() -> Int? { #if FOUNDATION_FRAMEWORK - if let interopChild = interopChild { - return interopChild.totalCount + switch interopType { + case .interopMirror(let mirror): + return mirror.totalCount + case .observation(let observation): + return selfFraction.total + case .none: + return selfFraction.total } -#endif +#else return selfFraction.total +#endif } /// Returns 0 if `self` has `nil` total units; /// returns a `Int` value otherwise. internal mutating func getCompletedCount() -> Int { #if FOUNDATION_FRAMEWORK - if let interopChild = interopChild { - return interopChild.completedCount + switch interopType { + case .interopMirror(let mirror): + return mirror.completedCount + case .observation(let observation): + updateChildrenProgressFraction() + return selfFraction.completed + case .none: + updateChildrenProgressFraction() + return selfFraction.completed } -#endif - +#else updateChildrenProgressFraction() - return selfFraction.completed +#endif } internal mutating func getFractionCompleted() -> Double { #if FOUNDATION_FRAMEWORK - // change name later to interopMirror - if let interopChild = interopChild { - return interopChild.fractionCompleted + switch interopType { + case .interopMirror(let mirror): + return mirror.fractionCompleted + case .observation(let observation): + updateChildrenProgressFraction() + return selfFraction.fractionCompleted + case .none: + updateChildrenProgressFraction() + return selfFraction.fractionCompleted } -#endif - +#else updateChildrenProgressFraction() - return overallFraction.fractionCompleted - +#endif } internal func getIsIndeterminate() -> Bool { #if FOUNDATION_FRAMEWORK - if let interopChild = interopChild { - return interopChild.isIndeterminate + switch interopType { + case .interopMirror(let mirror): + return mirror.isIndeterminate + case .observation(let observation): + return selfFraction.isIndeterminate + case .none: + return selfFraction.isIndeterminate } -#endif +#else return selfFraction.isIndeterminate +#endif } internal func getIsFinished() -> Bool { #if FOUNDATION_FRAMEWORK - if let interopChild = interopChild { - return interopChild.isIndeterminate + switch interopType { + case .interopMirror(let mirror): + return mirror.isFinished + case .observation(let observation): + return selfFraction.isFinished + case .none: + return selfFraction.isFinished } -#endif +#else return selfFraction.isFinished +#endif } internal mutating func updateChildrenProgressFraction() { @@ -214,20 +241,27 @@ extension ProgressManager { selfFraction.completed += count #if FOUNDATION_FRAMEWORK - interopObservation.subprogressBridge?.manager.notifyObservers( - with: .fractionUpdated( - totalCount: selfFraction.total ?? 0, - completedCount: selfFraction.completed - ) - ) - - if let _ = interopObservation.reporterBridge { - notifyObservers( + switch interopType { + case .interopMirror(let mirror): + return + case .observation(let observation): + observation.subprogressBridge?.manager.notifyObservers( with: .fractionUpdated( totalCount: selfFraction.total ?? 0, completedCount: selfFraction.completed ) ) + + if let _ = observation.reporterBridge { + notifyObservers( + with: .fractionUpdated( + totalCount: selfFraction.total ?? 0, + completedCount: selfFraction.completed + ) + ) + } + case .none: + return } #endif } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 09f86239e..7108b4518 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -93,9 +93,8 @@ internal import _FoundationCollections propertiesInt: [:], propertiesDouble: [:], propertiesString: [:], - interopChild: nil, - interopObservation: InteropObservation(subprogressBridge: subprogressBridge), - observers: [] + observers: [], + interopType: .observation(InteropObservation(subprogressBridge: subprogressBridge)) ) self.state = Mutex(state) } diff --git a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift index bc4f02e79..e1c6eacdc 100644 --- a/Sources/FoundationEssentials/ProgressManager/Subprogress.swift +++ b/Sources/FoundationEssentials/ProgressManager/Subprogress.swift @@ -52,7 +52,7 @@ public struct Subprogress: ~Copyable, Sendable { ) guard subprogressBridge == nil else { - subprogressBridge?.manager.setInteropChild(interopChild: childManager) + subprogressBridge?.manager.setInteropChild(interopMirror: childManager) return childManager } #else From 7ba9e6d6cc121d612d622a5b8c9cf791e02e17e4 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 13 Aug 2025 17:08:11 -0700 Subject: [PATCH 04/24] reorder interopType switch statements --- .../ProgressManager+Interop.swift | 14 ++-- ...ProgressManager+Properties+Accessors.swift | 22 ++--- .../ProgressManager+State.swift | 83 +++++++++---------- .../ProgressManager/ProgressManager.swift | 2 +- 4 files changed, 61 insertions(+), 60 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index d87b602d2..41015e725 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -223,7 +223,7 @@ extension ProgressManager { internal enum InteropType { case interopMirror(ProgressManager) - case observation(InteropObservation) + case interopObservation(InteropObservation) } } @@ -256,15 +256,19 @@ extension ProgressManager { internal func addBridge(reporterBridge: ProgressReporterBridge? = nil, nsProgressBridge: Foundation.Progress? = nil) { state.withLock { state in + var interopObservation = InteropObservation(subprogressBridge: nil) + if let reporterBridge { - let interopObservation = InteropObservation(subprogressBridge: nil, reporterBridge: reporterBridge) - state.interopType = .observation(interopObservation) + interopObservation.reporterBridge = reporterBridge +// state.interopObservation.reporterBridge = reporterBridge } if let nsProgressBridge { - let interopObservation = InteropObservation(subprogressBridge: nil, nsProgressBridge: nsProgressBridge) - state.interopType = .observation(interopObservation) + interopObservation.nsProgressBridge = nsProgressBridge +// state.interopObservation.nsProgressBridge = nsProgressBridge } + + state.interopType = .interopObservation(interopObservation) } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index fad00269f..25f617c36 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -147,7 +147,7 @@ extension ProgressManager { propertiesDouble: [:], propertiesString: [:], observers: [], - interopType: nil + interopType: nil, ) #else state = State( @@ -218,13 +218,13 @@ extension ProgressManager { #if FOUNDATION_FRAMEWORK if let observerState = values.observerState { switch state.interopType { - case .interopMirror: - break - case .observation(let interopObservation): - if let _ = interopObservation.reporterBridge { + case .interopObservation(let observation): + if let _ = observation.reporterBridge { notifyObservers(with: observerState) } - case .none: + case .interopMirror(let mirror): + break + default: break } } @@ -495,14 +495,14 @@ extension ProgressManager { #if FOUNDATION_FRAMEWORK private mutating func interopNotifications() { switch state.interopType { + case .interopObservation(let observation): + observation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) + self.observerState = .fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed) case .interopMirror(let mirror): break - case .observation(let interopObservation): - interopObservation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) - case .none: - break + default: + break } - self.observerState = .fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed) } #endif } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 3a724e286..037a26726 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -136,17 +136,16 @@ extension ProgressManager { /// returns a `Int` value otherwise. internal func getTotalCount() -> Int? { #if FOUNDATION_FRAMEWORK - switch interopType { - case .interopMirror(let mirror): - return mirror.totalCount - case .observation(let observation): - return selfFraction.total - case .none: - return selfFraction.total - } -#else - return selfFraction.total + switch interopType { + case .interopMirror(let mirror): + return mirror.totalCount + case .interopObservation(let observation): + break + default: + break + } #endif + return selfFraction.total } /// Returns 0 if `self` has `nil` total units; @@ -156,35 +155,34 @@ extension ProgressManager { switch interopType { case .interopMirror(let mirror): return mirror.completedCount - case .observation(let observation): - updateChildrenProgressFraction() - return selfFraction.completed - case .none: - updateChildrenProgressFraction() - return selfFraction.completed + case .interopObservation(let observation): + break + default: + break } -#else +#endif updateChildrenProgressFraction() return selfFraction.completed -#endif } internal mutating func getFractionCompleted() -> Double { #if FOUNDATION_FRAMEWORK + // change name later to interopMirror switch interopType { case .interopMirror(let mirror): return mirror.fractionCompleted - case .observation(let observation): - updateChildrenProgressFraction() - return selfFraction.fractionCompleted - case .none: - updateChildrenProgressFraction() - return selfFraction.fractionCompleted + case .interopObservation(let observation): + break + default: + break } -#else +// if let interopChild = interopChild { +// return interopChild.fractionCompleted +// } +#endif + updateChildrenProgressFraction() return overallFraction.fractionCompleted -#endif } internal func getIsIndeterminate() -> Bool { @@ -192,14 +190,14 @@ extension ProgressManager { switch interopType { case .interopMirror(let mirror): return mirror.isIndeterminate - case .observation(let observation): - return selfFraction.isIndeterminate - case .none: - return selfFraction.isIndeterminate + case .interopObservation(let observation): + break + default: + break } -#else - return selfFraction.isIndeterminate + #endif + return selfFraction.isIndeterminate } internal func getIsFinished() -> Bool { @@ -207,14 +205,13 @@ extension ProgressManager { switch interopType { case .interopMirror(let mirror): return mirror.isFinished - case .observation(let observation): - return selfFraction.isFinished - case .none: - return selfFraction.isFinished + case .interopObservation(let observation): + break + default: + break } -#else - return selfFraction.isFinished #endif + return selfFraction.isFinished } internal mutating func updateChildrenProgressFraction() { @@ -242,9 +239,7 @@ extension ProgressManager { #if FOUNDATION_FRAMEWORK switch interopType { - case .interopMirror(let mirror): - return - case .observation(let observation): + case .interopObservation(let observation): observation.subprogressBridge?.manager.notifyObservers( with: .fractionUpdated( totalCount: selfFraction.total ?? 0, @@ -260,8 +255,10 @@ extension ProgressManager { ) ) } - case .none: - return + case .interopMirror(let mirror): + break + default: + break } #endif } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 7108b4518..0c8cafb91 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -94,7 +94,7 @@ internal import _FoundationCollections propertiesDouble: [:], propertiesString: [:], observers: [], - interopType: .observation(InteropObservation(subprogressBridge: subprogressBridge)) + interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge)) ) self.state = Mutex(state) } From afcf85d37cbae697b33c864b745644926c11e8ad Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 14 Aug 2025 11:10:07 -0700 Subject: [PATCH 05/24] move interop switch-case into enum --- .../ProgressManager+Interop.swift | 45 +++++++++++++++ ...ProgressManager+Properties+Accessors.swift | 4 +- .../ProgressManager+State.swift | 55 ++++--------------- 3 files changed, 59 insertions(+), 45 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index 41015e725..25b68b106 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -224,6 +224,51 @@ extension ProgressManager { 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 + } + } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index 25f617c36..13265d2bd 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -222,7 +222,7 @@ extension ProgressManager { if let _ = observation.reporterBridge { notifyObservers(with: observerState) } - case .interopMirror(let mirror): + case .interopMirror: break default: break @@ -498,7 +498,7 @@ extension ProgressManager { case .interopObservation(let observation): observation.subprogressBridge?.manager.notifyObservers(with:.fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed)) self.observerState = .fractionUpdated(totalCount: state.selfFraction.total ?? 0, completedCount: state.selfFraction.completed) - case .interopMirror(let mirror): + case .interopMirror: break default: break diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 037a26726..df7a8c8ba 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -136,14 +136,9 @@ extension ProgressManager { /// returns a `Int` value otherwise. internal func getTotalCount() -> Int? { #if FOUNDATION_FRAMEWORK - switch interopType { - case .interopMirror(let mirror): - return mirror.totalCount - case .interopObservation(let observation): - break - default: - break - } + if let interopTotalCount = interopType?.totalCount { + return interopTotalCount + } #endif return selfFraction.total } @@ -152,13 +147,8 @@ extension ProgressManager { /// returns a `Int` value otherwise. internal mutating func getCompletedCount() -> Int { #if FOUNDATION_FRAMEWORK - switch interopType { - case .interopMirror(let mirror): - return mirror.completedCount - case .interopObservation(let observation): - break - default: - break + if let interopCompletedCount = interopType?.completedCount { + return interopCompletedCount } #endif updateChildrenProgressFraction() @@ -167,48 +157,27 @@ extension ProgressManager { internal mutating func getFractionCompleted() -> Double { #if FOUNDATION_FRAMEWORK - // change name later to interopMirror - switch interopType { - case .interopMirror(let mirror): - return mirror.fractionCompleted - case .interopObservation(let observation): - break - default: - break + if let interopFractionCompleted = interopType?.fractionCompleted { + return interopFractionCompleted } -// if let interopChild = interopChild { -// return interopChild.fractionCompleted -// } #endif - updateChildrenProgressFraction() return overallFraction.fractionCompleted } internal func getIsIndeterminate() -> Bool { #if FOUNDATION_FRAMEWORK - switch interopType { - case .interopMirror(let mirror): - return mirror.isIndeterminate - case .interopObservation(let observation): - break - default: - break + if let interopIsIndeterminate = interopType?.isIndeterminate { + return interopIsIndeterminate } - #endif return selfFraction.isIndeterminate } internal func getIsFinished() -> Bool { #if FOUNDATION_FRAMEWORK - switch interopType { - case .interopMirror(let mirror): - return mirror.isFinished - case .interopObservation(let observation): - break - default: - break + if let interopIsFinished = interopType?.isFinished { + return interopIsFinished } #endif return selfFraction.isFinished @@ -255,7 +224,7 @@ extension ProgressManager { ) ) } - case .interopMirror(let mirror): + case .interopMirror: break default: break From 69ea882b6e68802abfd11162b8f7ca043c4b84ea Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 14 Aug 2025 11:31:41 -0700 Subject: [PATCH 06/24] change Throughput and fileURL Summary types --- ...ProgressManager+Properties+Accessors.swift | 7 +++--- ...ogressManager+Properties+Definitions.swift | 25 ++++++++----------- .../ProgressManager+Properties+Helpers.swift | 10 ++++---- .../ProgressManager+State.swift | 4 +-- .../ProgressManager/ProgressReporter.swift | 4 +-- .../ProgressManagerPropertiesTests.swift | 10 ++++---- 6 files changed, 27 insertions(+), 33 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index 13265d2bd..dffde8aab 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -90,9 +90,8 @@ extension ProgressManager { /// /// - Note: The throughput is calculated as the sum of all throughput values divided by the count /// of progress managers that have throughput data. - public func summary(of property: ProgressManager.Properties.Throughput.Type) -> UInt64 { - let throughput = getUpdatedThroughput() - return throughput.values / UInt64(throughput.count) + public func summary(of property: ProgressManager.Properties.Throughput.Type) -> [UInt64] { + return getUpdatedThroughput() } /// Returns the maximum estimated time remaining for completion across the progress subtree. @@ -110,7 +109,7 @@ extension ProgressManager { /// /// - Parameter property: The `FileURL` property type. /// - Returns: An array containing all file URLs across the entire progress subtree. - public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL] { + public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL?] { return getUpdatedFileURL() } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift index e4a4fe59d..47592729c 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -181,25 +181,20 @@ extension ProgressManager { public struct Throughput: Sendable, Property { public typealias Value = UInt64 - public struct AggregateThroughput: Sendable, Equatable { - var values: UInt64 - var count: Int - } - - public typealias Summary = AggregateThroughput + 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: AggregateThroughput { return AggregateThroughput(values: 0, count: 0) } + public static var defaultSummary: [UInt64] { return [] } - public static func reduce(into summary: inout AggregateThroughput, value: UInt64) { - summary = Summary(values: summary.values + value, count: summary.count + 1) + public static func reduce(into summary: inout [UInt64], value: UInt64) { + summary.append(value) } - public static func merge(_ summary1: AggregateThroughput, _ summary2: AggregateThroughput) -> AggregateThroughput { - return Summary(values: summary1.values + summary2.values, count: summary1.count + summary2.count) + public static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { + return summary1 + summary2 } } @@ -237,22 +232,22 @@ extension ProgressManager { public typealias Value = URL? - public typealias Summary = [URL] + public typealias Summary = [URL?] public static var key: String { return "Foundation.ProgressManager.Properties.FileURL" } public static var defaultValue: URL? { return nil } - public static var defaultSummary: [URL] { return [] } + public static var defaultSummary: [URL?] { return [] } - public static func reduce(into summary: inout [URL], value: URL?) { + public static func reduce(into summary: inout [URL?], value: URL?) { guard let value else { return } summary.append(value) } - public static func merge(_ summary1: [URL], _ summary2: [URL]) -> [URL] { + public static func merge(_ summary1: [URL?], _ summary2: [URL?]) -> [URL?] { return summary1 + summary2 } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index 7c5a79b50..ff07edc4a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -286,10 +286,10 @@ extension ProgressManager { } } - internal func getUpdatedThroughput() -> ProgressManager.Properties.Throughput.AggregateThroughput { + internal func getUpdatedThroughput() -> [UInt64] { return state.withLock { state in // Get self's throughput as part of summary - var value: ProgressManager.Properties.Throughput.AggregateThroughput = ProgressManager.Properties.Throughput.defaultSummary + var value = ProgressManager.Properties.Throughput.defaultSummary ProgressManager.Properties.Throughput.reduce(into: &value, value: state.throughput) guard !state.children.isEmpty else { @@ -342,10 +342,10 @@ extension ProgressManager { } } - internal func getUpdatedFileURL() -> [URL] { + internal func getUpdatedFileURL() -> [URL?] { return state.withLock { state in // Get self's estimatedTimeRemaining as part of summary - var value: [URL] = ProgressManager.Properties.FileURL.defaultSummary + var value: [URL?] = ProgressManager.Properties.FileURL.defaultSummary ProgressManager.Properties.FileURL.reduce(into: &value, value: state.fileURL) guard !state.children.isEmpty else { @@ -554,7 +554,7 @@ extension ProgressManager { } } - internal func setChildThroughput(value: ProgressManager.Properties.Throughput.AggregateThroughput, at position: Int) { + internal func setChildThroughput(value: [UInt64], at position: Int) { state.withLock { state in state.children[position].throughput = PropertyStateThroughput(value: value, isDirty: false) } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index df7a8c8ba..42ff309b1 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -52,7 +52,7 @@ extension ProgressManager { } internal struct PropertyStateThroughput { - var value: ProgressManager.Properties.Throughput.AggregateThroughput + var value: [UInt64] var isDirty: Bool } @@ -72,7 +72,7 @@ extension ProgressManager { } internal struct PropertyStateURL { - var value: [URL] + var value: [URL?] var isDirty: Bool } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 3a3195b6b..17aadc836 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -150,7 +150,7 @@ import Observation /// /// - Parameter property: The `Throughput` property type. /// - Returns: The average throughput across the entire progress subtree, in bytes per second. - public func summary(of property: ProgressManager.Properties.Throughput.Type) -> UInt64 { + public func summary(of property: ProgressManager.Properties.Throughput.Type) -> [UInt64] { manager.summary(of: property) } @@ -166,7 +166,7 @@ import Observation /// /// - Parameter property: The `FileURL` property type. /// - Returns: An array containing all file URLs across the entire progress subtree. - public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL] { + public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL?] { manager.summary(of: property) } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index 8992e51fe..e49caed6b 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -256,7 +256,7 @@ import Testing } #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 2000) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [2000]) } func doSomethingTwoLevels(subprogress: consuming Subprogress) async { @@ -270,7 +270,7 @@ import Testing await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 1500) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 2000]) } @Test func discreteManager() async throws { @@ -283,7 +283,7 @@ import Testing } #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 3000) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [3000]) } @Test func twoLevelManager() async throws { @@ -296,7 +296,7 @@ import Testing await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == 1500) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 2000]) } @Test func threeLevelManager() async throws { @@ -310,7 +310,7 @@ import Testing await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == Int64(4000 / 3)) + #expect(manager.summary(of: ProgressManager.Properties.Throughput.self) == [1000, 1000, 2000]) } } From 5409507790b6c320d5b6f96f7d1657f22479f649 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 14 Aug 2025 11:42:01 -0700 Subject: [PATCH 07/24] remove old unused calls --- .../ProgressManager/ProgressManager+Interop.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift index 25b68b106..b1faea98e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Interop.swift @@ -305,12 +305,10 @@ extension ProgressManager { if let reporterBridge { interopObservation.reporterBridge = reporterBridge -// state.interopObservation.reporterBridge = reporterBridge } if let nsProgressBridge { interopObservation.nsProgressBridge = nsProgressBridge -// state.interopObservation.nsProgressBridge = nsProgressBridge } state.interopType = .interopObservation(interopObservation) From cf6ea71a15374a974c42b385a3886581d704d976 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 08:30:59 -0700 Subject: [PATCH 08/24] introduce terminate method requirement + rewrite implementation for additional property --- ...ogressManager+Properties+Definitions.swift | 30 +++ .../ProgressManager+Properties+Helpers.swift | 190 ++++++++---------- .../ProgressManager+State.swift | 5 +- .../ProgressManager/ProgressManager.swift | 29 ++- .../ProgressManagerPropertiesTests.swift | 12 ++ 5 files changed, 149 insertions(+), 117 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift index 47592729c..f5c10d2d5 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -79,6 +79,9 @@ extension ProgressManager { /// - 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 the behavior for handling childSummary when the child is deinitialized. + static func terminate(_ parentSummary: Summary, _ childSummary: Summary) -> Summary } // Namespace for properties specific to operations reported on @@ -105,6 +108,10 @@ extension ProgressManager { public static func merge(_ summary1: Int, _ summary2: Int) -> Int { return summary1 + summary2 } + + public static func terminate(_ parentSummary: Int, _ childSummary: Int) -> Int { + return parentSummary + childSummary + } } /// The number of completed files. @@ -128,6 +135,10 @@ extension ProgressManager { public static func merge(_ summary1: Int, _ summary2: Int) -> Int { return summary1 + summary2 } + + public static func terminate(_ parentSummary: Int, _ childSummary: Int) -> Int { + return parentSummary + childSummary + } } /// The total number of bytes. @@ -151,6 +162,10 @@ extension ProgressManager { public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { return summary1 + summary2 } + + public static func terminate(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { + return parentSummary + childSummary + } } /// The number of completed bytes. @@ -174,6 +189,10 @@ extension ProgressManager { public static func merge(_ summary1: UInt64, _ summary2: UInt64) -> UInt64 { return summary1 + summary2 } + + public static func terminate(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { + return parentSummary + childSummary + } } /// The throughput, in bytes per second. @@ -196,6 +215,10 @@ extension ProgressManager { public static func merge(_ summary1: [UInt64], _ summary2: [UInt64]) -> [UInt64] { return summary1 + summary2 } + + public static func terminate(_ parentSummary: [UInt64], _ childSummary: [UInt64]) -> [UInt64] { + return parentSummary + childSummary + } } /// The amount of time remaining in the processing of files. @@ -223,6 +246,10 @@ extension ProgressManager { public static func merge(_ summary1: Duration, _ summary2: Duration) -> Duration { return max(summary1, summary2) } + + public static func terminate(_ parentSummary: Duration, _ childSummary: Duration) -> Duration { + return parentSummary + } } @@ -251,6 +278,9 @@ extension ProgressManager { return summary1 + summary2 } + public static func terminate(_ parentSummary: [URL?], _ childSummary: [URL?]) -> [URL?] { + return parentSummary + } } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index ff07edc4a..2da377730 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -33,34 +33,30 @@ extension ProgressManager { for (idx, childState) in state.children.enumerated() { if let childPropertyState = childState.childPropertiesInt[property] { if childPropertyState.isDirty { + // Dirty, needs to fetch value if let child = childState.child { let updatedSummary = child.getUpdatedIntSummary(property: property) let newChildPropertyState = PropertyStateInt(value: updatedSummary, isDirty: false) state.children[idx].childPropertiesInt[property] = newChildPropertyState value = property.merge(value, updatedSummary) - } else { - if let remainingProperties = childState.remainingPropertiesInt { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } else { - value = property.merge(value, childPropertyState.value) + // Not dirty, use value directly + if let _ = childState.child { + value = property.merge(value, childPropertyState.value) + } else { + // TODO: What to do after terminate? Set to nil? + value = property.terminate(value, childPropertyState.value) + } } } else { + // Said property doesn't even get cached yet, but children might have been set if let child = childState.child { + // If there is a child let childSummary = child.getUpdatedIntSummary(property: property) let newChildPropertyState = PropertyStateInt(value: childSummary, isDirty: false) state.children[idx].childPropertiesInt[property] = newChildPropertyState value = property.merge(value, childSummary) - } else { - // Get value from remainingProperties - if let remainingProperties = childState.remainingPropertiesInt { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } } @@ -87,17 +83,14 @@ extension ProgressManager { let newChildPropertyState = PropertyStateDouble(value: updatedSummary, isDirty: false) state.children[idx].childPropertiesDouble[property] = newChildPropertyState value = property.merge(value, updatedSummary) - } else { - // Get value from remainingProperties - if let remainingProperties = childState.remainingPropertiesDouble { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } else { - // Merge non-dirty, updated value - value = property.merge(value, childPropertyState.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } else { + value = property.terminate(value, childPropertyState.value) + } } } else { // First fetch of value @@ -106,13 +99,6 @@ extension ProgressManager { let newChildPropertyState = PropertyStateDouble(value: childSummary, isDirty: false) state.children[idx].childPropertiesDouble[property] = newChildPropertyState value = property.merge(value, childSummary) - } else { - // Get value from remainingProperties - if let remainingProperties = childState.remainingPropertiesDouble { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } } @@ -139,17 +125,14 @@ extension ProgressManager { let newChildPropertyState = PropertyStateString(value: updatedSummary, isDirty: false) state.children[idx].childPropertiesString[property] = newChildPropertyState value = property.merge(value, updatedSummary) - } else { - // Get value from remainingProperties - if let remainingProperties = childState.remainingPropertiesString { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } else { - // Merge non-dirty, updated value - value = property.merge(value, childPropertyState.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } else { + value = property.terminate(value, childPropertyState.value) + } } } else { // First fetch of value @@ -158,13 +141,6 @@ extension ProgressManager { let newChildPropertyState = PropertyStateString(value: childSummary, isDirty: false) state.children[idx].childPropertiesString[property] = newChildPropertyState value = property.merge(value, childSummary) - } else { - // Get value from remainingProperties - if let remainingProperties = childState.remainingPropertiesString { - if let remainingSummary = remainingProperties[property] { - value = property.merge(value, remainingSummary) - } - } } } } @@ -194,8 +170,12 @@ extension ProgressManager { value = ProgressManager.Properties.TotalFileCount.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.TotalFileCount.merge(value, childState.totalFileCount.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.TotalFileCount.merge(value, childState.totalFileCount.value) + } else { + value = ProgressManager.Properties.TotalFileCount.terminate(value, childState.totalFileCount.value) + } } } return value @@ -220,8 +200,12 @@ extension ProgressManager { value = ProgressManager.Properties.CompletedFileCount.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.CompletedFileCount.merge(value, childState.completedFileCount.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.CompletedFileCount.merge(value, childState.completedFileCount.value) + } else { + value = ProgressManager.Properties.CompletedFileCount.terminate(value, childState.completedFileCount.value) + } } } return value @@ -251,8 +235,12 @@ extension ProgressManager { value = ProgressManager.Properties.TotalByteCount.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.TotalByteCount.merge(value, childState.totalByteCount.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.TotalByteCount.merge(value, childState.totalByteCount.value) + } else { + value = ProgressManager.Properties.TotalByteCount.terminate(value, childState.totalByteCount.value) + } } } return value @@ -277,8 +265,12 @@ extension ProgressManager { value = ProgressManager.Properties.CompletedByteCount.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.CompletedByteCount.merge(value, childState.completedByteCount.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.CompletedByteCount.merge(value, childState.completedByteCount.value) + } else { + value = ProgressManager.Properties.CompletedByteCount.terminate(value, childState.completedByteCount.value) + } } } return value @@ -306,8 +298,12 @@ extension ProgressManager { value = ProgressManager.Properties.Throughput.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.Throughput.merge(value, childState.throughput.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.Throughput.merge(value, childState.throughput.value) + } else { + value = ProgressManager.Properties.Throughput.terminate(value, childState.throughput.value) + } } } return value @@ -334,8 +330,12 @@ extension ProgressManager { value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, childState.estimatedTimeRemaining.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.EstimatedTimeRemaining.merge(value, childState.estimatedTimeRemaining.value) + } else { + value = ProgressManager.Properties.EstimatedTimeRemaining.terminate(value, childState.estimatedTimeRemaining.value) + } } } return value @@ -362,8 +362,12 @@ extension ProgressManager { value = ProgressManager.Properties.FileURL.merge(value, updatedSummary) } } else { - // Merge non-dirty, updated value - value = ProgressManager.Properties.FileURL.merge(value, childState.fileURL.value) + if let _ = childState.child { + // Merge non-dirty, updated value + value = ProgressManager.Properties.FileURL.merge(value, childState.fileURL.value) + } else { + value = ProgressManager.Properties.FileURL.terminate(value, childState.fileURL.value) + } } } return value @@ -511,52 +515,28 @@ extension ProgressManager { markSelfDirty(property: property, parents: parents) } - //MARK: Methods to preserve values of properties upon deinit - internal func setChildRemainingPropertiesInt(_ properties: [MetatypeWrapper: Int], at position: Int) { - state.withLock { state in - state.children[position].remainingPropertiesInt = properties - } - } - - internal func setChildRemainingPropertiesDouble(_ properties: [MetatypeWrapper: Double], at position: Int) { - state.withLock { state in - state.children[position].remainingPropertiesDouble = properties - } - } - - internal func setChildRemainingPropertiesString(_ properties: [MetatypeWrapper: String], at position: Int) { - state.withLock { state in - state.children[position].remainingPropertiesString = properties - } - } - - internal func setChildTotalFileCount(value: Int, at position: Int) { - state.withLock { state in - state.children[position].totalFileCount = PropertyStateInt(value: value, isDirty: false) - } - } - - internal func setChildCompletedFileCount(value: Int, at position: Int) { + //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, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: String]) { state.withLock { state in - state.children[position].completedFileCount = PropertyStateInt(value: value, isDirty: false) - } - } - - internal func setChildTotalByteCount(value: UInt64, at position: Int) { - state.withLock { state in - state.children[position].totalByteCount = PropertyStateUInt64(value: value, isDirty: false) - } - } - - internal func setChildCompletedByteCount(value: UInt64, at position: Int) { - state.withLock { state in - state.children[position].completedByteCount = PropertyStateUInt64(value: value, isDirty: false) - } - } - - internal func setChildThroughput(value: [UInt64], at position: Int) { - state.withLock { state in - state.children[position].throughput = PropertyStateThroughput(value: value, isDirty: false) + state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false) + state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false) + state.children[position].totalByteCount = PropertyStateUInt64(value: totalByteCount, isDirty: false) + state.children[position].completedByteCount = PropertyStateUInt64(value: completedByteCount, isDirty: false) + state.children[position].throughput = PropertyStateThroughput(value: throughput, isDirty: false) + state.children[position].estimatedTimeRemaining = PropertyStateDuration(value: estimatedTimeRemaining, isDirty: false) + state.children[position].fileURL = PropertyStateURL(value: fileURL, isDirty: false) + + for (propertyKey, propertyValue) in propertiesInt { + state.children[position].childPropertiesInt[propertyKey] = PropertyStateInt(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesDouble { + state.children[position].childPropertiesDouble[propertyKey] = PropertyStateDouble(value: propertyValue, isDirty: false) + } + + for (propertyKey, propertyValue) in propertiesString { + state.children[position].childPropertiesString[propertyKey] = PropertyStateString(value: propertyValue, isDirty: false) + } } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 42ff309b1..9c50d864f 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -18,6 +18,7 @@ extension ProgressManager { let reduce: @Sendable (inout T, T) -> () let merge: @Sendable (T, T) -> T + let terminate: @Sendable (T, T) -> T let defaultValue: T let defaultSummary: T @@ -27,6 +28,7 @@ extension ProgressManager { init(_ argument: P.Type) where P.Value == T, P.Summary == T { reduce = P.reduce merge = P.merge + terminate = P.terminate defaultValue = P.defaultValue defaultSummary = P.defaultSummary key = P.key @@ -78,9 +80,6 @@ extension ProgressManager { internal struct ChildState { weak var child: ProgressManager? - var remainingPropertiesInt: [MetatypeWrapper: Int]? - var remainingPropertiesDouble: [MetatypeWrapper: Double]? - var remainingPropertiesString: [MetatypeWrapper: String]? var portionOfTotal: Int var childFraction: ProgressFraction var isDirty: Bool diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 0c8cafb91..c8c581c1e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -231,7 +231,6 @@ internal import _FoundationCollections internal func addChild(child: ProgressManager, portion: Int, childFraction: ProgressFraction) -> Int { let (index, parents) = state.withLock { state in let childState = ChildState(child: child, - remainingPropertiesInt: nil, portionOfTotal: portion, childFraction: childFraction, isDirty: true, @@ -324,16 +323,28 @@ internal import _FoundationCollections finalSummaryString[property] = updatedSummary } + let totalFileCount = self.getUpdatedFileCount(type: .total) + let completedFileCount = self.getUpdatedFileCount(type: .completed) + let totalByteCount = self.getUpdatedByteCount(type: .total) + let completedByteCount = self.getUpdatedByteCount(type: .completed) + let throughput = self.getUpdatedThroughput() + let estimatedTimeRemaining = self.getUpdatedEstimatedTimeRemaining() + let fileURL = self.getUpdatedFileURL() for parentState in parents { - parentState.parent.setChildRemainingPropertiesInt(finalSummaryInt, at: parentState.positionInParent) - parentState.parent.setChildRemainingPropertiesDouble(finalSummaryDouble, at: parentState.positionInParent) - parentState.parent.setChildRemainingPropertiesString(finalSummaryString, at: parentState.positionInParent) - parentState.parent.setChildTotalFileCount(value: self.getUpdatedFileCount(type: .total), at: parentState.positionInParent) - parentState.parent.setChildCompletedFileCount(value: self.getUpdatedFileCount(type: .completed), at: parentState.positionInParent) - parentState.parent.setChildTotalByteCount(value: self.getUpdatedByteCount(type: .total), at: parentState.positionInParent) - parentState.parent.setChildCompletedByteCount(value: self.getUpdatedByteCount(type: .completed), at: parentState.positionInParent) - parentState.parent.setChildThroughput(value: self.getUpdatedThroughput(), at: parentState.positionInParent) + parentState.parent.setChildDeclaredAdditionalProperties( + at: parentState.positionInParent, + totalFileCount: totalFileCount, + completedFileCount: completedFileCount, + totalByteCount: totalByteCount, + completedByteCount: completedByteCount, + throughput: throughput, + estimatedTimeRemaining: estimatedTimeRemaining, + fileURL: fileURL, + propertiesInt: finalSummaryInt, + propertiesDouble: finalSummaryDouble, + propertiesString: finalSummaryString + ) } } } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index e49caed6b..ca5a4d1bc 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -459,6 +459,10 @@ extension ProgressManager.Properties { static func merge(_ summary1: Int, _ summary2: Int) -> Int { return summary1 + summary2 } + + static func terminate(_ parentSummary: Int, _ childSummary: Int) -> Int { + return parentSummary + childSummary + } } } @@ -560,6 +564,10 @@ extension ProgressManager.Properties { static func merge(_ summary1: Double, _ summary2: Double) -> Double { return summary1 + summary2 } + + static func terminate(_ parentSummary: Double, _ childSummary: Double) -> Double { + return parentSummary + childSummary + } } } @@ -659,6 +667,10 @@ extension ProgressManager.Properties { static func merge(_ summary1: String, _ summary2: String) -> String { return summary1 + ", " + summary2 } + + static func terminate(_ parentSummary: String, _ childSummary: String) -> String { + return parentSummary + ", " + childSummary + } } } From 866947f6dd1fba75f66fb8b671d246fb9d10e245 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 09:03:10 -0700 Subject: [PATCH 09/24] change additional property string summary to [String] --- ...ProgressManager+Properties+Accessors.swift | 10 +++---- .../ProgressManager+Properties+Helpers.swift | 22 +++++++------- .../ProgressManager+State.swift | 30 +++++++++---------- .../ProgressManager/ProgressManager.swift | 6 ++-- .../ProgressManager/ProgressReporter.swift | 4 +-- .../ProgressManagerPropertiesTests.swift | 26 ++++++++-------- 6 files changed, 49 insertions(+), 49 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index dffde8aab..46fa26e33 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -47,7 +47,7 @@ extension ProgressManager { /// - 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) -> P.Summary where P.Value == String, P.Summary == String { + public func summary(of property: P.Type) -> P.Summary where P.Value == String, P.Summary == [String] { return getUpdatedStringSummary(property: MetatypeWrapper(property)) } @@ -247,9 +247,9 @@ extension ProgressManager { internal var throughputDirty = false internal var estimatedTimeRemainingDirty = false internal var fileURLDirty = false - internal var dirtyPropertiesInt: [MetatypeWrapper] = [] - internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] - internal var dirtyPropertiesString: [MetatypeWrapper] = [] + internal var dirtyPropertiesInt: [MetatypeWrapper] = [] + internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] + internal var dirtyPropertiesString: [MetatypeWrapper] = [] #if FOUNDATION_FRAMEWORK internal var observerState: ObserverState? #endif @@ -476,7 +476,7 @@ extension ProgressManager { /// 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 { + public subscript(dynamicMember key: KeyPath) -> String where P.Value == String, P.Summary == [String] { get { return state.propertiesString[MetatypeWrapper(P.self)] ?? P.self.defaultValue } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index 2da377730..be65b0a8d 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -20,7 +20,7 @@ extension ProgressManager { } //MARK: Methods to get updated summary of properties - internal func getUpdatedIntSummary(property: MetatypeWrapper) -> Int { + internal func getUpdatedIntSummary(property: MetatypeWrapper) -> Int { return state.withLock { state in var value: Int = property.defaultSummary @@ -64,7 +64,7 @@ extension ProgressManager { } } - internal func getUpdatedDoubleSummary(property: MetatypeWrapper) -> Double { + internal func getUpdatedDoubleSummary(property: MetatypeWrapper) -> Double { return state.withLock { state in var value: Double = property.defaultSummary @@ -106,10 +106,10 @@ extension ProgressManager { } } - internal func getUpdatedStringSummary(property: MetatypeWrapper) -> String { + internal func getUpdatedStringSummary(property: MetatypeWrapper) -> [String] { return state.withLock { state in - var value: String = property.defaultSummary + var value: [String] = property.defaultSummary property.reduce(&value, state.propertiesString[property] ?? property.defaultValue) guard !state.children.isEmpty else { @@ -375,19 +375,19 @@ extension ProgressManager { } //MARK: Methods to set dirty bit recursively - internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) } } - internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) } } - internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) } @@ -435,7 +435,7 @@ extension ProgressManager { } } - internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { let parents = state.withLock { state in state.children[position].childPropertiesInt[property]?.isDirty = true return state.parents @@ -443,7 +443,7 @@ extension ProgressManager { markSelfDirty(property: property, parents: parents) } - internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { let parents = state.withLock { state in state.children[position].childPropertiesDouble[property]?.isDirty = true return state.parents @@ -451,7 +451,7 @@ extension ProgressManager { markSelfDirty(property: property, parents: parents) } - internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { let parents = state.withLock { state in state.children[position].childPropertiesString[property]?.isDirty = true return state.parents @@ -516,7 +516,7 @@ extension ProgressManager { } //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, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: String]) { + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String]]) { state.withLock { state in state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false) state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 9c50d864f..c35326903 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -14,18 +14,18 @@ internal import Synchronization @available(FoundationPreview 6.2, *) extension ProgressManager { - internal struct MetatypeWrapper: Hashable, Equatable, Sendable { + internal struct MetatypeWrapper: Hashable, Equatable, Sendable { - let reduce: @Sendable (inout T, T) -> () - let merge: @Sendable (T, T) -> T - let terminate: @Sendable (T, T) -> T + let reduce: @Sendable (inout S, V) -> () + let merge: @Sendable (S, S) -> S + let terminate: @Sendable (S, S) -> S - let defaultValue: T - let defaultSummary: T + let defaultValue: V + let defaultSummary: S let key: String - init(_ argument: P.Type) where P.Value == T, P.Summary == T { + init(_ argument: P.Type) where P.Value == V, P.Summary == S { reduce = P.reduce merge = P.merge terminate = P.terminate @@ -38,7 +38,7 @@ extension ProgressManager { hasher.combine(key) } - static func == (lhs: ProgressManager.MetatypeWrapper, rhs: ProgressManager.MetatypeWrapper) -> Bool { + static func == (lhs: ProgressManager.MetatypeWrapper, rhs: ProgressManager.MetatypeWrapper) -> Bool { lhs.key == rhs.key } } @@ -69,7 +69,7 @@ extension ProgressManager { } internal struct PropertyStateString { - var value: String + var value: [String] var isDirty: Bool } @@ -90,9 +90,9 @@ extension ProgressManager { var throughput: PropertyStateThroughput var estimatedTimeRemaining: PropertyStateDuration var fileURL: PropertyStateURL - var childPropertiesInt: [MetatypeWrapper: PropertyStateInt] - var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] - var childPropertiesString: [MetatypeWrapper: PropertyStateString] + var childPropertiesInt: [MetatypeWrapper: PropertyStateInt] + var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] + var childPropertiesString: [MetatypeWrapper: PropertyStateString] } internal struct ParentState { @@ -123,9 +123,9 @@ extension ProgressManager { var throughput: UInt64 var estimatedTimeRemaining: Duration var fileURL: URL? - var propertiesInt: [MetatypeWrapper: Int] - var propertiesDouble: [MetatypeWrapper: Double] - var propertiesString: [MetatypeWrapper: String] + var propertiesInt: [MetatypeWrapper: Int] + var propertiesDouble: [MetatypeWrapper: Double] + var propertiesString: [MetatypeWrapper: String] #if FOUNDATION_FRAMEWORK var observers: [@Sendable (ObserverState) -> Void] var interopType: InteropType? diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index c8c581c1e..31a1b8b6b 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -305,19 +305,19 @@ internal import _FoundationCollections return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.parents) } - var finalSummaryInt: [MetatypeWrapper: Int] = [:] + var finalSummaryInt: [MetatypeWrapper: Int] = [:] for property in propertiesInt.keys { let updatedSummary = self.getUpdatedIntSummary(property: property) finalSummaryInt[property] = updatedSummary } - var finalSummaryDouble: [MetatypeWrapper: Double] = [:] + var finalSummaryDouble: [MetatypeWrapper: Double] = [:] for property in propertiesDouble.keys { let updatedSummary = self.getUpdatedDoubleSummary(property: property) finalSummaryDouble[property] = updatedSummary } - var finalSummaryString: [MetatypeWrapper: String] = [:] + var finalSummaryString: [MetatypeWrapper: [String]] = [:] for property in propertiesString.keys { let updatedSummary = self.getUpdatedStringSummary(property: property) finalSummaryString[property] = updatedSummary diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 17aadc836..41577a73d 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -110,8 +110,8 @@ import Observation /// - 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 { - manager.summary(of: property) + public func summary(of property: P.Type) -> [String] where P.Value == String, P.Summary == [String] { + return manager.summary(of: property) } /// Returns the total file count across the progress subtree. diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index ca5a4d1bc..d330937f2 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -652,24 +652,24 @@ extension ProgressManager.Properties { typealias Value = String - typealias Summary = String + typealias Summary = [String] static var key: String { return "FileName" } static var defaultValue: String { return "" } - static var defaultSummary: String { return "" } + static var defaultSummary: [String] { return [] } - static func reduce(into summary: inout String, value: String) { - summary += value + 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 merge(_ summary1: [String], _ summary2: [String]) -> [String] { + return summary1 + summary2 } - static func terminate(_ parentSummary: String, _ childSummary: String) -> String { - return parentSummary + ", " + childSummary + static func terminate(_ parentSummary: [String], _ childSummary: [String]) -> [String] { + return parentSummary + childSummary } } } @@ -686,7 +686,7 @@ extension ProgressManager.Properties { } #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Melon.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Melon.jpg"]) } func doSomethingTwoLevels(subprogress: consuming Subprogress) async { @@ -700,7 +700,7 @@ extension ProgressManager.Properties { await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Cherry.jpg, Melon.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Cherry.jpg", "Melon.jpg"]) } @Test func discreteManager() async throws { @@ -713,7 +713,7 @@ extension ProgressManager.Properties { #expect(manager.fractionCompleted == 1.0) #expect(manager.withProperties { $0.fileName } == "Grape.jpg") - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Grape.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Grape.jpg"]) } @Test func twoLevelsManager() async throws { @@ -727,7 +727,7 @@ extension ProgressManager.Properties { await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Watermelon.jpg, Melon.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Watermelon.jpg", "Melon.jpg"]) } @Test func threeLevelsManager() async throws { @@ -741,6 +741,6 @@ extension ProgressManager.Properties { await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == "Watermelon.jpg, Cherry.jpg, Melon.jpg") + #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Watermelon.jpg", "Cherry.jpg", "Melon.jpg"]) } } From c1492ebd2666660d0a55167c26cafc21f4bd8768 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 14:05:30 -0700 Subject: [PATCH 10/24] change string type to use String? and [String?] --- .../ProgressManager+Properties+Accessors.swift | 6 +++--- .../ProgressManager+Properties+Helpers.swift | 10 +++++----- .../ProgressManager/ProgressManager+State.swift | 6 +++--- .../ProgressManager/ProgressManager.swift | 2 +- .../ProgressManager/ProgressReporter.swift | 2 +- .../ProgressManagerPropertiesTests.swift | 14 +++++++------- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index 46fa26e33..e957ef3d0 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -47,7 +47,7 @@ extension ProgressManager { /// - 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) -> P.Summary where P.Value == String, P.Summary == [String] { + public func summary(of property: P.Type) -> P.Summary where P.Value == String?, P.Summary == [String?] { return getUpdatedStringSummary(property: MetatypeWrapper(property)) } @@ -249,7 +249,7 @@ extension ProgressManager { internal var fileURLDirty = false internal var dirtyPropertiesInt: [MetatypeWrapper] = [] internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] - internal var dirtyPropertiesString: [MetatypeWrapper] = [] + internal var dirtyPropertiesString: [MetatypeWrapper] = [] #if FOUNDATION_FRAMEWORK internal var observerState: ObserverState? #endif @@ -476,7 +476,7 @@ extension ProgressManager { /// 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] { + public subscript(dynamicMember key: KeyPath) -> String? where P.Value == String?, P.Summary == [String?] { get { return state.propertiesString[MetatypeWrapper(P.self)] ?? P.self.defaultValue } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index be65b0a8d..5ac3ca8ab 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -106,10 +106,10 @@ extension ProgressManager { } } - internal func getUpdatedStringSummary(property: MetatypeWrapper) -> [String] { + internal func getUpdatedStringSummary(property: MetatypeWrapper) -> [String?] { return state.withLock { state in - var value: [String] = property.defaultSummary + var value: [String?] = property.defaultSummary property.reduce(&value, state.propertiesString[property] ?? property.defaultValue) guard !state.children.isEmpty else { @@ -387,7 +387,7 @@ extension ProgressManager { } } - internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) } @@ -451,7 +451,7 @@ extension ProgressManager { markSelfDirty(property: property, parents: parents) } - internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { let parents = state.withLock { state in state.children[position].childPropertiesString[property]?.isDirty = true return state.parents @@ -516,7 +516,7 @@ extension ProgressManager { } //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, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String]]) { + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]]) { state.withLock { state in state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false) state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index c35326903..a10795adb 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -69,7 +69,7 @@ extension ProgressManager { } internal struct PropertyStateString { - var value: [String] + var value: [String?] var isDirty: Bool } @@ -92,7 +92,7 @@ extension ProgressManager { var fileURL: PropertyStateURL var childPropertiesInt: [MetatypeWrapper: PropertyStateInt] var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] - var childPropertiesString: [MetatypeWrapper: PropertyStateString] + var childPropertiesString: [MetatypeWrapper: PropertyStateString] } internal struct ParentState { @@ -125,7 +125,7 @@ extension ProgressManager { var fileURL: URL? var propertiesInt: [MetatypeWrapper: Int] var propertiesDouble: [MetatypeWrapper: Double] - var propertiesString: [MetatypeWrapper: String] + var propertiesString: [MetatypeWrapper: String?] #if FOUNDATION_FRAMEWORK var observers: [@Sendable (ObserverState) -> Void] var interopType: InteropType? diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 31a1b8b6b..1cf7e84e1 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -317,7 +317,7 @@ internal import _FoundationCollections finalSummaryDouble[property] = updatedSummary } - var finalSummaryString: [MetatypeWrapper: [String]] = [:] + var finalSummaryString: [MetatypeWrapper: [String?]] = [:] for property in propertiesString.keys { let updatedSummary = self.getUpdatedStringSummary(property: property) finalSummaryString[property] = updatedSummary diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 41577a73d..22dbac7b6 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -110,7 +110,7 @@ import Observation /// - 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] { + public func summary(of property: P.Type) -> [String?] where P.Value == String?, P.Summary == [String?] { return manager.summary(of: property) } diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index d330937f2..5ca9b1165 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -650,25 +650,25 @@ extension ProgressManager.Properties { var fileName: FileName.Type { FileName.self } struct FileName: Sendable, ProgressManager.Property { - typealias Value = String + typealias Value = String? - typealias Summary = [String] + typealias Summary = [String?] static var key: String { return "FileName" } - static var defaultValue: String { return "" } + static var defaultValue: String? { return "" } - static var defaultSummary: [String] { return [] } + static var defaultSummary: [String?] { return [] } - static func reduce(into summary: inout [String], value: String) { + static func reduce(into summary: inout [String?], value: String?) { summary.append(value) } - static func merge(_ summary1: [String], _ summary2: [String]) -> [String] { + static func merge(_ summary1: [String?], _ summary2: [String?]) -> [String?] { return summary1 + summary2 } - static func terminate(_ parentSummary: [String], _ childSummary: [String]) -> [String] { + static func terminate(_ parentSummary: [String?], _ childSummary: [String?]) -> [String?] { return parentSummary + childSummary } } From 545a9c2e0d026c3c805fa512070218481bbdd818 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 14:26:49 -0700 Subject: [PATCH 11/24] add propertiesURL as available additional property type --- ...ProgressManager+Properties+Accessors.swift | 31 +++++++++- .../ProgressManager+Properties+Helpers.swift | 62 ++++++++++++++++++- .../ProgressManager+State.swift | 2 + .../ProgressManager/ProgressManager.swift | 17 +++-- .../ProgressManager/ProgressReporter.swift | 4 ++ 5 files changed, 110 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index e957ef3d0..f77b7c6d9 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -51,6 +51,10 @@ extension ProgressManager { return getUpdatedStringSummary(property: MetatypeWrapper(property)) } + public func summary(of property: P.Type) -> P.Summary where P.Value == URL?, P.Summary == [URL?] { + return getUpdatedURLSummary(property: MetatypeWrapper(property)) + } + /// Returns the total file count across the progress subtree. /// /// - Parameter property: The `TotalFileCount` property type. @@ -145,6 +149,7 @@ extension ProgressManager { propertiesInt: [:], propertiesDouble: [:], propertiesString: [:], + propertiesURL: [:], observers: [], interopType: nil, ) @@ -161,7 +166,8 @@ extension ProgressManager { estimatedTimeRemaining: ProgressManager.Properties.EstimatedTimeRemaining.defaultValue, propertiesInt: [:], propertiesDouble: [:], - propertiesString: [:] + propertiesString: [:], + propertiesURL: [:] ) #endif let result = try closure(&values) @@ -214,6 +220,12 @@ extension ProgressManager { markSelfDirty(property: property, parents: values.state.parents) } } + + if values.dirtyPropertiesURL.count > 0 { + for property in values.dirtyPropertiesURL { + markSelfDirty(property: property, parents: values.state.parents) + } + } #if FOUNDATION_FRAMEWORK if let observerState = values.observerState { switch state.interopType { @@ -250,6 +262,7 @@ extension ProgressManager { internal var dirtyPropertiesInt: [MetatypeWrapper] = [] internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] internal var dirtyPropertiesString: [MetatypeWrapper] = [] + internal var dirtyPropertiesURL: [MetatypeWrapper] = [] #if FOUNDATION_FRAMEWORK internal var observerState: ObserverState? #endif @@ -491,6 +504,22 @@ extension ProgressManager { dirtyPropertiesString.append(MetatypeWrapper(P.self)) } } + + public subscript(dynamicMember key: KeyPath) -> URL? where P.Value == URL?, P.Summary == [URL?] { + get { + return state.propertiesURL[MetatypeWrapper(P.self)] ?? P.self.defaultValue + } + + set { + guard newValue != state.propertiesURL[MetatypeWrapper(P.self)] else { + return + } + + state.propertiesURL[MetatypeWrapper(P.self)] = newValue + + dirtyPropertiesURL.append(MetatypeWrapper(P.self)) + } + } #if FOUNDATION_FRAMEWORK private mutating func interopNotifications() { switch state.interopType { diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index 5ac3ca8ab..1656772ba 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -148,6 +148,48 @@ extension ProgressManager { } } + internal func getUpdatedURLSummary(property: MetatypeWrapper) -> [URL?] { + return state.withLock { state in + + var value: [URL?] = property.defaultSummary + property.reduce(&value, state.propertiesURL[property] ?? property.defaultValue) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if let childPropertyState = childState.childPropertiesURL[property] { + if childPropertyState.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedURLSummary(property: property) + let newChildPropertyState = PropertyStateURL(value: updatedSummary, isDirty: false) + state.children[idx].childPropertiesURL[property] = newChildPropertyState + value = property.merge(value, updatedSummary) + } + } else { + if let _ = childState.child { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } else { + value = property.terminate(value, childPropertyState.value) + } + } + } else { + // First fetch of value + if let child = childState.child { + let childSummary = child.getUpdatedURLSummary(property: property) + let newChildPropertyState = PropertyStateURL(value: childSummary, isDirty: false) + state.children[idx].childPropertiesURL[property] = newChildPropertyState + value = property.merge(value, childSummary) + } + } + } + return value + } + } + internal func getUpdatedFileCount(type: CountType) -> Int { switch type { case .total: @@ -393,6 +435,12 @@ extension ProgressManager { } } + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) @@ -458,6 +506,14 @@ extension ProgressManager { } markSelfDirty(property: property, parents: parents) } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + let parents = state.withLock { state in + state.children[position].childPropertiesURL[property]?.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) { let parents = state.withLock { state in @@ -516,7 +572,7 @@ extension ProgressManager { } //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, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]]) { + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]], propertiesURL: [MetatypeWrapper: [URL?]]) { state.withLock { state in state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false) state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false) @@ -537,6 +593,10 @@ extension ProgressManager { for (propertyKey, propertyValue) in propertiesString { state.children[position].childPropertiesString[propertyKey] = PropertyStateString(value: propertyValue, isDirty: false) } + + for (propertyKey, propertyValue) in propertiesURL { + state.children[position].childPropertiesURL[propertyKey] = PropertyStateURL(value: propertyValue, isDirty: false) + } } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index a10795adb..e25618dcc 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -93,6 +93,7 @@ extension ProgressManager { var childPropertiesInt: [MetatypeWrapper: PropertyStateInt] var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] var childPropertiesString: [MetatypeWrapper: PropertyStateString] + var childPropertiesURL: [MetatypeWrapper: PropertyStateURL] } internal struct ParentState { @@ -126,6 +127,7 @@ extension ProgressManager { var propertiesInt: [MetatypeWrapper: Int] var propertiesDouble: [MetatypeWrapper: Double] var propertiesString: [MetatypeWrapper: String?] + var propertiesURL: [MetatypeWrapper: URL?] #if FOUNDATION_FRAMEWORK var observers: [@Sendable (ObserverState) -> Void] var interopType: InteropType? diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 1cf7e84e1..7c764fca2 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -93,6 +93,7 @@ internal import _FoundationCollections propertiesInt: [:], propertiesDouble: [:], propertiesString: [:], + propertiesURL: [:], observers: [], interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge)) ) @@ -243,7 +244,8 @@ internal import _FoundationCollections fileURL: PropertyStateURL(value: ProgressManager.Properties.FileURL.defaultSummary, isDirty: false), childPropertiesInt: [:], childPropertiesDouble: [:], - childPropertiesString: [:]) + childPropertiesString: [:], + childPropertiesURL: [:]) state.children.append(childState) return (state.children.count - 1, state.parents) } @@ -301,8 +303,8 @@ internal import _FoundationCollections } } - let (propertiesInt, propertiesDouble, propertiesString, parents) = state.withLock { state in - return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.parents) + let (propertiesInt, propertiesDouble, propertiesString, propertiesURL, parents) = state.withLock { state in + return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.parents) } var finalSummaryInt: [MetatypeWrapper: Int] = [:] @@ -323,6 +325,12 @@ internal import _FoundationCollections finalSummaryString[property] = updatedSummary } + var finalSummaryURL: [MetatypeWrapper: [URL?]] = [:] + for property in propertiesURL.keys { + let updatedSummary = self.getUpdatedURLSummary(property: property) + finalSummaryURL[property] = updatedSummary + } + let totalFileCount = self.getUpdatedFileCount(type: .total) let completedFileCount = self.getUpdatedFileCount(type: .completed) let totalByteCount = self.getUpdatedByteCount(type: .total) @@ -343,7 +351,8 @@ internal import _FoundationCollections fileURL: fileURL, propertiesInt: finalSummaryInt, propertiesDouble: finalSummaryDouble, - propertiesString: finalSummaryString + propertiesString: finalSummaryString, + propertiesURL: finalSummaryURL ) } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 22dbac7b6..b75e6c32a 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -114,6 +114,10 @@ import Observation return manager.summary(of: property) } + public func summary(of property: P.Type) -> [URL?] where P.Value == URL?, P.Summary == [URL?] { + return manager.summary(of: property) + } + /// Returns the total file count across the progress subtree. /// /// - Parameter property: The `TotalFileCount` property type. From 6df4e75a75f9e882c488da4e881f8acc5d7b0f7b Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 14:46:00 -0700 Subject: [PATCH 12/24] formatting for readability --- ...ProgressManager+Properties+Accessors.swift | 228 +++++++++--------- .../ProgressManager+Properties+Helpers.swift | 6 +- 2 files changed, 120 insertions(+), 114 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index f77b7c6d9..57190d97e 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -15,118 +15,7 @@ internal import Synchronization @available(FoundationPreview 6.2, *) extension ProgressManager { - /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. - public func summary(of property: P.Type) -> P.Summary where P.Value == Int, P.Summary == Int { - return getUpdatedIntSummary(property: MetatypeWrapper(property)) - } - - /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. - public func summary(of property: P.Type) -> P.Summary where P.Value == Double, P.Summary == Double { - return getUpdatedDoubleSummary(property: MetatypeWrapper(property)) - } - - /// Returns a summary for the specified 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 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) -> P.Summary where P.Value == String?, P.Summary == [String?] { - return getUpdatedStringSummary(property: MetatypeWrapper(property)) - } - - public func summary(of property: P.Type) -> P.Summary where P.Value == URL?, P.Summary == [URL?] { - return getUpdatedURLSummary(property: MetatypeWrapper(property)) - } - - /// Returns the total file count across the progress subtree. - /// - /// - Parameter property: The `TotalFileCount` property type. - /// - Returns: The sum of all total file counts across the entire progress subtree. - public func summary(of property: ProgressManager.Properties.TotalFileCount.Type) -> Int { - return getUpdatedFileCount(type: .total) - } - - /// Returns the completed file count across the progress subtree. - /// - /// - Parameter property: The `CompletedFileCount` property type. - /// - Returns: The sum of all completed file counts across the entire progress subtree. - public func summary(of property: ProgressManager.Properties.CompletedFileCount.Type) -> Int { - return getUpdatedFileCount(type: .completed) - } - - /// Returns the total byte count across the progress subtree. - /// - /// - Parameter property: The `TotalByteCount` property type. - /// - Returns: The sum of all total byte counts across the entire progress subtree, in bytes. - public func summary(of property: ProgressManager.Properties.TotalByteCount.Type) -> UInt64 { - return getUpdatedByteCount(type: .total) - } - - /// Returns the completed byte count across the progress subtree. - /// - /// - Parameter property: The `CompletedByteCount` property type. - /// - Returns: The sum of all completed byte counts across the entire progress subtree, in bytes. - public func summary(of property: ProgressManager.Properties.CompletedByteCount.Type) -> UInt64 { - return getUpdatedByteCount(type: .completed) - } - - /// Returns the average throughput across the progress subtree. - /// - /// - Parameter property: The `Throughput` property type. - /// - Returns: The average throughput across the entire progress subtree, in bytes per second. - /// - /// - Note: The throughput is calculated as the sum of all throughput values divided by the count - /// of progress managers that have throughput data. - public func summary(of property: ProgressManager.Properties.Throughput.Type) -> [UInt64] { - return getUpdatedThroughput() - } - - /// Returns the maximum estimated time remaining for completion across the progress subtree. - /// - /// - Parameter property: The `EstimatedTimeRemaining` property type. - /// - Returns: The estimated duration until completion for the entire progress subtree. - /// - /// - Note: The estimation is based on current throughput and remaining work. The accuracy - /// depends on the consistency of the processing rate. - public func summary(of property: ProgressManager.Properties.EstimatedTimeRemaining.Type) -> Duration { - return getUpdatedEstimatedTimeRemaining() - } - - /// Returns all file URLs being processed across the progress subtree. - /// - /// - Parameter property: The `FileURL` property type. - /// - Returns: An array containing all file URLs across the entire progress subtree. - public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL?] { - return getUpdatedFileURL() - } - - // MARK: Additional Properties Methods - internal func getProperties( - _ closure: (sending Values) throws(E) -> sending T - ) throws(E) -> sending T { - try state.withLock { state throws(E) -> T in - let values = Values(state: state) - let result = try closure(values) - return result - } - } + // MARK: Methods to Read & Write Additional Properties of single ProgressManager node /// Mutates any settable properties that convey information about progress. public func withProperties( @@ -520,6 +409,7 @@ extension ProgressManager { dirtyPropertiesURL.append(MetatypeWrapper(P.self)) } } + #if FOUNDATION_FRAMEWORK private mutating func interopNotifications() { switch state.interopType { @@ -534,4 +424,118 @@ extension ProgressManager { } #endif } + + internal func getProperties( + _ closure: (sending Values) throws(E) -> sending T + ) throws(E) -> sending T { + try state.withLock { state throws(E) -> T in + let values = Values(state: state) + let result = try closure(values) + return result + } + } + + // MARK: Methods to Read Additional Properties of Subtree with ProgressManager as root + + /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> P.Summary where P.Value == Int, P.Summary == Int { + return getUpdatedIntSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for the specified 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: The aggregated summary value for the specified property across the entire subtree. + public func summary(of property: P.Type) -> P.Summary where P.Value == Double, P.Summary == Double { + return getUpdatedDoubleSummary(property: MetatypeWrapper(property)) + } + + /// Returns a summary for the specified 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 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) -> P.Summary where P.Value == String?, P.Summary == [String?] { + return getUpdatedStringSummary(property: MetatypeWrapper(property)) + } + + public func summary(of property: P.Type) -> P.Summary where P.Value == URL?, P.Summary == [URL?] { + return getUpdatedURLSummary(property: MetatypeWrapper(property)) + } + + /// Returns the total file count across the progress subtree. + /// + /// - Parameter property: The `TotalFileCount` property type. + /// - Returns: The sum of all total file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.TotalFileCount.Type) -> Int { + return getUpdatedFileCount(type: .total) + } + + /// Returns the completed file count across the progress subtree. + /// + /// - Parameter property: The `CompletedFileCount` property type. + /// - Returns: The sum of all completed file counts across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.CompletedFileCount.Type) -> Int { + return getUpdatedFileCount(type: .completed) + } + + /// Returns the total byte count across the progress subtree. + /// + /// - Parameter property: The `TotalByteCount` property type. + /// - Returns: The sum of all total byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.TotalByteCount.Type) -> UInt64 { + return getUpdatedByteCount(type: .total) + } + + /// Returns the completed byte count across the progress subtree. + /// + /// - Parameter property: The `CompletedByteCount` property type. + /// - Returns: The sum of all completed byte counts across the entire progress subtree, in bytes. + public func summary(of property: ProgressManager.Properties.CompletedByteCount.Type) -> UInt64 { + return getUpdatedByteCount(type: .completed) + } + + /// Returns the average throughput across the progress subtree. + /// + /// - Parameter property: The `Throughput` property type. + /// - Returns: The average throughput across the entire progress subtree, in bytes per second. + /// + /// - Note: The throughput is calculated as the sum of all throughput values divided by the count + /// of progress managers that have throughput data. + public func summary(of property: ProgressManager.Properties.Throughput.Type) -> [UInt64] { + return getUpdatedThroughput() + } + + /// Returns the maximum estimated time remaining for completion across the progress subtree. + /// + /// - Parameter property: The `EstimatedTimeRemaining` property type. + /// - Returns: The estimated duration until completion for the entire progress subtree. + /// + /// - Note: The estimation is based on current throughput and remaining work. The accuracy + /// depends on the consistency of the processing rate. + public func summary(of property: ProgressManager.Properties.EstimatedTimeRemaining.Type) -> Duration { + return getUpdatedEstimatedTimeRemaining() + } + + /// Returns all file URLs being processed across the progress subtree. + /// + /// - Parameter property: The `FileURL` property type. + /// - Returns: An array containing all file URLs across the entire progress subtree. + public func summary(of property: ProgressManager.Properties.FileURL.Type) -> [URL?] { + return getUpdatedFileURL() + } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index 1656772ba..222748ac8 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -19,7 +19,8 @@ extension ProgressManager { case completed } - //MARK: Methods to get updated summary of properties + //MARK: Helper Methods for Updating Dirty Path + internal func getUpdatedIntSummary(property: MetatypeWrapper) -> Int { return state.withLock { state in @@ -416,7 +417,8 @@ extension ProgressManager { } } - //MARK: Methods to set dirty bit recursively + //MARK: Helper Methods for Setting Dirty Paths + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) From 1380f2b60781b82ce3661b4156abfcc46d0c2a70 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 15 Aug 2025 16:16:39 -0700 Subject: [PATCH 13/24] add propertiesUInt64 as available additional property type --- ...ProgressManager+Properties+Accessors.swift | 31 +++++++++- .../ProgressManager+Properties+Helpers.swift | 62 ++++++++++++++++++- .../ProgressManager+State.swift | 2 + .../ProgressManager/ProgressManager.swift | 17 +++-- .../ProgressManager/ProgressReporter.swift | 4 ++ 5 files changed, 110 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index 57190d97e..aa7766ac3 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -39,6 +39,7 @@ extension ProgressManager { propertiesDouble: [:], propertiesString: [:], propertiesURL: [:], + propertiesUInt64: [:], observers: [], interopType: nil, ) @@ -56,7 +57,8 @@ extension ProgressManager { propertiesInt: [:], propertiesDouble: [:], propertiesString: [:], - propertiesURL: [:] + propertiesURL: [:], + propertiesUInt64: [:] ) #endif let result = try closure(&values) @@ -115,6 +117,12 @@ extension ProgressManager { markSelfDirty(property: property, parents: values.state.parents) } } + + if values.dirtyPropertiesUInt64.count > 0 { + for property in values.dirtyPropertiesUInt64 { + markSelfDirty(property: property, parents: values.state.parents) + } + } #if FOUNDATION_FRAMEWORK if let observerState = values.observerState { switch state.interopType { @@ -152,6 +160,7 @@ extension ProgressManager { internal var dirtyPropertiesDouble: [MetatypeWrapper] = [] internal var dirtyPropertiesString: [MetatypeWrapper] = [] internal var dirtyPropertiesURL: [MetatypeWrapper] = [] + internal var dirtyPropertiesUInt64: [MetatypeWrapper] = [] #if FOUNDATION_FRAMEWORK internal var observerState: ObserverState? #endif @@ -410,6 +419,22 @@ extension ProgressManager { } } + public subscript(dynamicMember key: KeyPath) -> UInt64? where P.Value == UInt64, P.Summary == [UInt64] { + get { + return state.propertiesUInt64[MetatypeWrapper(P.self)] ?? P.self.defaultValue + } + + set { + guard newValue != state.propertiesUInt64[MetatypeWrapper(P.self)] else { + return + } + + state.propertiesUInt64[MetatypeWrapper(P.self)] = newValue + + dirtyPropertiesUInt64.append(MetatypeWrapper(P.self)) + } + } + #if FOUNDATION_FRAMEWORK private mutating func interopNotifications() { switch state.interopType { @@ -477,6 +502,10 @@ extension ProgressManager { return getUpdatedURLSummary(property: MetatypeWrapper(property)) } + public func summary(of property: P.Type) -> P.Summary where P.Value == UInt64, P.Summary == [UInt64] { + return getUpdatedUInt64Summary(property: MetatypeWrapper(property)) + } + /// Returns the total file count across the progress subtree. /// /// - Parameter property: The `TotalFileCount` property type. diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift index 222748ac8..ae8c899c5 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Helpers.swift @@ -191,6 +191,48 @@ extension ProgressManager { } } + internal func getUpdatedUInt64Summary(property: MetatypeWrapper) -> [UInt64] { + return state.withLock { state in + + var value: [UInt64] = property.defaultSummary + property.reduce(&value, state.propertiesUInt64[property] ?? property.defaultValue) + + guard !state.children.isEmpty else { + return value + } + + for (idx, childState) in state.children.enumerated() { + if let childPropertyState = childState.childPropertiesUInt64[property] { + if childPropertyState.isDirty { + // Update dirty path + if let child = childState.child { + let updatedSummary = child.getUpdatedUInt64Summary(property: property) + let newChildPropertyState = PropertyStateThroughput(value: updatedSummary, isDirty: false) + state.children[idx].childPropertiesUInt64[property] = newChildPropertyState + value = property.merge(value, updatedSummary) + } + } else { + if let _ = childState.child { + // Merge non-dirty, updated value + value = property.merge(value, childPropertyState.value) + } else { + value = property.terminate(value, childPropertyState.value) + } + } + } else { + // First fetch of value + if let child = childState.child { + let childSummary = child.getUpdatedUInt64Summary(property: property) + let newChildPropertyState = PropertyStateThroughput(value: childSummary, isDirty: false) + state.children[idx].childPropertiesUInt64[property] = newChildPropertyState + value = property.merge(value, childSummary) + } + } + } + return value + } + } + internal func getUpdatedFileCount(type: CountType) -> Int { switch type { case .total: @@ -443,6 +485,12 @@ extension ProgressManager { } } + internal func markSelfDirty(property: MetatypeWrapper, parents: [ParentState]) { + for parentState in parents { + parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) + } + } + internal func markSelfDirty(property: ProgressManager.Properties.TotalFileCount.Type, parents: [ParentState]) { for parentState in parents { parentState.parent.markChildDirty(property: property, at: parentState.positionInParent) @@ -516,6 +564,14 @@ extension ProgressManager { } markSelfDirty(property: property, parents: parents) } + + internal func markChildDirty(property: MetatypeWrapper, at position: Int) { + let parents = state.withLock { state in + state.children[position].childPropertiesUInt64[property]?.isDirty = true + return state.parents + } + markSelfDirty(property: property, parents: parents) + } internal func markChildDirty(property: ProgressManager.Properties.TotalFileCount.Type, at position: Int) { let parents = state.withLock { state in @@ -574,7 +630,7 @@ extension ProgressManager { } //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, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]], propertiesURL: [MetatypeWrapper: [URL?]]) { + internal func setChildDeclaredAdditionalProperties(at position: Int, totalFileCount: Int, completedFileCount: Int, totalByteCount: UInt64, completedByteCount: UInt64, throughput: [UInt64], estimatedTimeRemaining: Duration, fileURL: [URL?], propertiesInt: [MetatypeWrapper: Int], propertiesDouble: [MetatypeWrapper: Double], propertiesString: [MetatypeWrapper: [String?]], propertiesURL: [MetatypeWrapper: [URL?]], propertiesUInt64: [MetatypeWrapper: [UInt64]]) { state.withLock { state in state.children[position].totalFileCount = PropertyStateInt(value: totalFileCount, isDirty: false) state.children[position].completedFileCount = PropertyStateInt(value: completedFileCount, isDirty: false) @@ -599,6 +655,10 @@ extension ProgressManager { for (propertyKey, propertyValue) in propertiesURL { state.children[position].childPropertiesURL[propertyKey] = PropertyStateURL(value: propertyValue, isDirty: false) } + + for (propertyKey, propertyValue) in propertiesUInt64 { + state.children[position].childPropertiesUInt64[propertyKey] = PropertyStateThroughput(value: propertyValue, isDirty: false) + } } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index e25618dcc..0e76be9d9 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -94,6 +94,7 @@ extension ProgressManager { var childPropertiesDouble: [MetatypeWrapper: PropertyStateDouble] var childPropertiesString: [MetatypeWrapper: PropertyStateString] var childPropertiesURL: [MetatypeWrapper: PropertyStateURL] + var childPropertiesUInt64: [MetatypeWrapper: PropertyStateThroughput] } internal struct ParentState { @@ -128,6 +129,7 @@ extension ProgressManager { var propertiesDouble: [MetatypeWrapper: Double] var propertiesString: [MetatypeWrapper: String?] var propertiesURL: [MetatypeWrapper: URL?] + var propertiesUInt64: [MetatypeWrapper: UInt64] #if FOUNDATION_FRAMEWORK var observers: [@Sendable (ObserverState) -> Void] var interopType: InteropType? diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 7c764fca2..83d900fb2 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -94,6 +94,7 @@ internal import _FoundationCollections propertiesDouble: [:], propertiesString: [:], propertiesURL: [:], + propertiesUInt64: [:], observers: [], interopType: .interopObservation(InteropObservation(subprogressBridge: subprogressBridge)) ) @@ -245,7 +246,8 @@ internal import _FoundationCollections childPropertiesInt: [:], childPropertiesDouble: [:], childPropertiesString: [:], - childPropertiesURL: [:]) + childPropertiesURL: [:], + childPropertiesUInt64: [:]) state.children.append(childState) return (state.children.count - 1, state.parents) } @@ -303,8 +305,8 @@ internal import _FoundationCollections } } - let (propertiesInt, propertiesDouble, propertiesString, propertiesURL, parents) = state.withLock { state in - return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.parents) + let (propertiesInt, propertiesDouble, propertiesString, propertiesURL, propertiesUInt64, parents) = state.withLock { state in + return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.propertiesUInt64, state.parents) } var finalSummaryInt: [MetatypeWrapper: Int] = [:] @@ -331,6 +333,12 @@ internal import _FoundationCollections finalSummaryURL[property] = updatedSummary } + var finalSummaryUInt64: [MetatypeWrapper: [UInt64]] = [:] + for property in propertiesUInt64.keys { + let updatedSummary = self.getUpdatedUInt64Summary(property: property) + finalSummaryUInt64[property] = updatedSummary + } + let totalFileCount = self.getUpdatedFileCount(type: .total) let completedFileCount = self.getUpdatedFileCount(type: .completed) let totalByteCount = self.getUpdatedByteCount(type: .total) @@ -352,7 +360,8 @@ internal import _FoundationCollections propertiesInt: finalSummaryInt, propertiesDouble: finalSummaryDouble, propertiesString: finalSummaryString, - propertiesURL: finalSummaryURL + propertiesURL: finalSummaryURL, + propertiesUInt64: finalSummaryUInt64 ) } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index b75e6c32a..845a644bc 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -118,6 +118,10 @@ import Observation return manager.summary(of: property) } + public func summary(of property: P.Type) -> [UInt64] where P.Value == UInt64, P.Summary == [UInt64] { + return manager.summary(of: property) + } + /// Returns the total file count across the progress subtree. /// /// - Parameter property: The `TotalFileCount` property type. From cd8e01014f3d880142bdb26856842ecd3d7fbefd Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 18 Aug 2025 18:50:54 -0700 Subject: [PATCH 14/24] deinit test for additional properties + fix for deinit fractionCompleted calculation --- .../ProgressManager/ProgressManager+State.swift | 12 +++++++----- .../ProgressManager/ProgressManager.swift | 9 +++------ .../ProgressManagerPropertiesTests.swift | 8 ++++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 0e76be9d9..967c6ea23 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -106,11 +106,13 @@ extension ProgressManager { var selfFraction: ProgressFraction var overallFraction: ProgressFraction { var overallFraction = selfFraction - for child in children { - if !child.childFraction.isFinished { - let multiplier = ProgressFraction(completed: child.portionOfTotal, total: selfFraction.total) - if let additionalFraction = multiplier * child.childFraction { - overallFraction = overallFraction + additionalFraction + for childState in children { + if let _ = childState.child { + if !childState.childFraction.isFinished { + let multiplier = ProgressFraction(completed: childState.portionOfTotal, total: selfFraction.total) + if let additionalFraction = multiplier * childState.childFraction { + overallFraction = overallFraction + additionalFraction + } } } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 83d900fb2..77431ebb2 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -297,12 +297,9 @@ internal import _FoundationCollections } deinit { - if !isFinished { - self.withProperties { properties in - if let totalCount = properties.totalCount { - properties.completedCount = totalCount - } - } + if let totalCount = self.totalCount { + let diff = totalCount - self.completedCount + self.complete(count: diff) } let (propertiesInt, propertiesDouble, propertiesString, propertiesURL, propertiesUInt64, parents) = state.withLock { state in diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index 5ca9b1165..c025f1183 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -425,15 +425,19 @@ import Testing properties.fileURL = URL(string: "https://www.cats.com") } - let childManager = manager.subprogress(assigningCount: 1).start(totalCount: 2) + var childManager: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 2) - childManager.withProperties { properties in + childManager?.withProperties { properties in properties.completedCount = 1 properties.fileURL = URL(string: "https://www.kittens.com") } #expect(manager.fractionCompleted == 0.75) #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.cats.com"), URL(string: "https://www.kittens.com")]) + + childManager = nil + #expect(manager.fractionCompleted == 1.0) + #expect(manager.summary(of: ProgressManager.Properties.FileURL.self) == [URL(string: "https://www.cats.com")]) } } From 374ec32c372be8749cc801692a44aad28b36e08d Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Mon, 18 Aug 2025 18:55:07 -0700 Subject: [PATCH 15/24] edit test to test deinit behavior for EstimatedTimeRemaining --- .../ProgressManager/ProgressManagerPropertiesTests.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index c025f1183..b4a90b8c6 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -365,14 +365,18 @@ import Testing properties.estimatedTimeRemaining = Duration.seconds(200) } - let child = manager.subprogress(assigningCount: 1).start(totalCount: 2) - child.withProperties { properties in + var child: ProgressManager? = manager.subprogress(assigningCount: 1).start(totalCount: 2) + child?.withProperties { properties in properties.completedCount = 1 properties.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)) } } From f24b8b17e1792af95726bd6e397672620f7a407e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 19 Aug 2025 18:02:18 -0700 Subject: [PATCH 16/24] replace redundant deinit complete(count:) call with mark dirty + add tests for deinit bahavior --- .../ProgressManager+State.swift | 3 +- .../ProgressManager/ProgressManager.swift | 10 +++---- .../ProgressManagerTests.swift | 28 +++++++++++++++++++ 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift index 967c6ea23..0e09af181 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+State.swift @@ -179,12 +179,13 @@ extension ProgressManager { return selfFraction.isIndeterminate } - internal func getIsFinished() -> Bool { + internal mutating func getIsFinished() -> Bool { #if FOUNDATION_FRAMEWORK if let interopIsFinished = interopType?.isFinished { return interopIsFinished } #endif + updateChildrenProgressFraction() return selfFraction.isFinished } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 77431ebb2..3e372c513 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -297,11 +297,7 @@ internal import _FoundationCollections } deinit { - if let totalCount = self.totalCount { - let diff = totalCount - self.completedCount - self.complete(count: diff) - } - + let (propertiesInt, propertiesDouble, propertiesString, propertiesURL, propertiesUInt64, parents) = state.withLock { state in return (state.propertiesInt, state.propertiesDouble, state.propertiesString, state.propertiesURL, state.propertiesUInt64, state.parents) } @@ -344,6 +340,10 @@ internal import _FoundationCollections let estimatedTimeRemaining = self.getUpdatedEstimatedTimeRemaining() let fileURL = self.getUpdatedFileURL() + if !isFinished { + markSelfDirty(parents: parents) + } + for parentState in parents { parentState.parent.setChildDeclaredAdditionalProperties( at: parentState.positionInParent, diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 650ea9751..6d189fc06 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -330,6 +330,7 @@ extension Tag { } } + // MARK: Test deinit behavior func makeUnfinishedChild(subprogress: consuming Subprogress) async { let manager = subprogress.start(totalCount: 3) manager.complete(count: 2) @@ -345,4 +346,31 @@ extension Tag { await makeUnfinishedChild(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) } + + func makeFinishedChild(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + manager.complete(count: 2) + } + + @Test func finishedChild() 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 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) + } } From 08f34f28902037d098f43c45d627aa5bb8d278f8 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Tue, 19 Aug 2025 18:11:19 -0700 Subject: [PATCH 17/24] more deinit tests --- .../ProgressManager/ProgressManagerTests.swift | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index 6d189fc06..fc8c7b38a 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -352,7 +352,7 @@ extension Tag { manager.complete(count: 2) } - @Test func finishedChild() async throws { + @Test func finishedChildUnreadBeforeDeinit() async throws { let manager = ProgressManager(totalCount: 2) manager.complete(count: 1) @@ -362,6 +362,20 @@ extension Tag { #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) From cb26bca84581b1c9f587e0ccf2ffa1a88e98eccc Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 20 Aug 2025 10:56:55 -0700 Subject: [PATCH 18/24] additional unit tests for deinit behavior --- .../ProgressManagerTests.swift | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift index fc8c7b38a..6f8b8fd49 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerTests.swift @@ -337,6 +337,11 @@ extension Tag { #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) @@ -347,9 +352,24 @@ extension Tag { #expect(manager.fractionCompleted == 1.0) } - func makeFinishedChild(subprogress: consuming Subprogress) async { - let manager = subprogress.start(totalCount: 2) - manager.complete(count: 2) + @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 { From 0ce022c2ff5b89d27001b0934c28e60ec7f44b11 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 20 Aug 2025 11:13:57 -0700 Subject: [PATCH 19/24] separate string additional properties into retaining vs non-retaining version --- .../ProgressManagerPropertiesTests.swift | 125 ++++++++++++++++-- 1 file changed, 112 insertions(+), 13 deletions(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index b4a90b8c6..f06c1c18f 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -655,8 +655,8 @@ extension ProgressManager.Properties { extension ProgressManager.Properties { - var fileName: FileName.Type { FileName.self } - struct FileName: Sendable, ProgressManager.Property { + var downloadedFile: DownloadedFile.Type { DownloadedFile.self } + struct DownloadedFile: Sendable, ProgressManager.Property { typealias Value = String? @@ -690,11 +690,11 @@ extension ProgressManager.Properties { manager.withProperties { properties in properties.completedCount += 1 - properties.fileName = "Melon.jpg" + properties.downloadedFile = "Melon.jpg" } #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Melon.jpg"]) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Melon.jpg"]) } func doSomethingTwoLevels(subprogress: consuming Subprogress) async { @@ -702,13 +702,13 @@ extension ProgressManager.Properties { manager.withProperties { properties in properties.completedCount = 1 - properties.fileName = "Cherry.jpg" + properties.downloadedFile = "Cherry.jpg" } await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Cherry.jpg", "Melon.jpg"]) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Cherry.jpg", "Melon.jpg"]) } @Test func discreteManager() async throws { @@ -716,12 +716,12 @@ extension ProgressManager.Properties { manager.withProperties { properties in properties.completedCount += 1 - properties.fileName = "Grape.jpg" + properties.downloadedFile = "Grape.jpg" } #expect(manager.fractionCompleted == 1.0) - #expect(manager.withProperties { $0.fileName } == "Grape.jpg") - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Grape.jpg"]) + #expect(manager.withProperties { $0.downloadedFile } == "Grape.jpg") + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Grape.jpg"]) } @Test func twoLevelsManager() async throws { @@ -729,13 +729,13 @@ extension ProgressManager.Properties { manager.withProperties { properties in properties.completedCount = 1 - properties.fileName = "Watermelon.jpg" + properties.downloadedFile = "Watermelon.jpg" } await doSomething(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Watermelon.jpg", "Melon.jpg"]) + #expect(manager.summary(of: ProgressManager.Properties.DownloadedFile.self) == ["Watermelon.jpg", "Melon.jpg"]) } @Test func threeLevelsManager() async throws { @@ -743,12 +743,111 @@ extension ProgressManager.Properties { manager.withProperties { properties in properties.completedCount = 1 - properties.fileName = "Watermelon.jpg" + properties.downloadedFile = "Watermelon.jpg" } await doSomethingTwoLevels(subprogress: manager.subprogress(assigningCount: 1)) #expect(manager.fractionCompleted == 1.0) - #expect(manager.summary(of: ProgressManager.Properties.FileName.self) == ["Watermelon.jpg", "Cherry.jpg", "Melon.jpg"]) + #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 "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 terminate(_ 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.withProperties { properties in + properties.completedCount += 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount += 1 + properties.processingFile = "Howdy.jpg" + } + + #expect(manager.fractionCompleted == 1.0) + #expect(manager.withProperties { $0.processingFile } == "Howdy.jpg") + #expect(manager.summary(of: ProgressManager.Properties.ProcessingFile.self) == ["Howdy.jpg"]) + } + + @Test func twoLevelsManager() async throws { + let manager = ProgressManager(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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"]) } } From 34ac15e34adcc8d2478602f2f7b2628db155681e Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 20 Aug 2025 15:01:11 -0700 Subject: [PATCH 20/24] add custom URL property unit tests --- .../ProgressManagerPropertiesTests.swift | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index f06c1c18f..25b415db8 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -683,7 +683,7 @@ extension ProgressManager.Properties { } -@Suite("Progress Manager String Properties", .tags(.progressManager)) struct ProgressManagerStringPropertiesTests { +@Suite("Progress Manager String (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerStringPropertiesTests { func doSomething(subprogress: consuming Subprogress) async { let manager = subprogress.start(totalCount: 1) @@ -851,3 +851,98 @@ extension ProgressManager.Properties { #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 terminate(_ parentSummary: [URL?], _ childSummary: [URL?]) -> [URL?] { + parentSummary + } + } +} + +@Suite("Progress Manager Image URL (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerImageURLProperties { + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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")]) + } +} + From e4a64ad73e61908f1b9adb424a66fbbcc95eb821 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Wed, 20 Aug 2025 15:39:15 -0700 Subject: [PATCH 21/24] add custom UInt64 property unit tests --- .../ProgressManagerPropertiesTests.swift | 94 ++++++++++++++++++- 1 file changed, 93 insertions(+), 1 deletion(-) diff --git a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift index 25b415db8..889893da4 100644 --- a/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift +++ b/Tests/FoundationEssentialsTests/ProgressManager/ProgressManagerPropertiesTests.swift @@ -881,7 +881,7 @@ extension ProgressManager.Properties { } } -@Suite("Progress Manager Image URL (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerImageURLProperties { +@Suite("Progress Manager URL (Non-retaining) Properties", .tags(.progressManager)) struct ProgressManagerURLProperties { func doSomething(subprogress: consuming Subprogress) async { let manager = subprogress.start(totalCount: 1) @@ -946,3 +946,95 @@ extension ProgressManager.Properties { } } +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 terminate(_ parentSummary: [UInt64], _ childSummary: [UInt64]) -> [UInt64] { + parentSummary + childSummary + } + } +} + +@Suite("Progress Manager UInt64 (Retaining) Properties", .tags(.progressManager)) struct ProgressManagerUInt64Properties { + + func doSomething(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 1) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.totalPixelCount = 24 + } + + #expect(manager.summary(of: ProgressManager.Properties.TotalPixelCount.self) == [24]) + } + + func doSomethingTwoLevels(subprogress: consuming Subprogress) async { + let manager = subprogress.start(totalCount: 2) + + manager.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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.withProperties { properties in + properties.completedCount = 1 + properties.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]) + } +} From 8a3197d1972b9f3518862e723438767fa0a3db56 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Thu, 21 Aug 2025 14:48:00 -0700 Subject: [PATCH 22/24] update documentation for additional properties methods --- ...ProgressManager+Properties+Accessors.swift | 50 +++++++++++--- ...ogressManager+Properties+Definitions.swift | 65 ++++++++++++------- .../ProgressManager/ProgressReporter.swift | 10 ++- 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index aa7766ac3..d222e4cdd 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -382,9 +382,9 @@ extension ProgressManager { /// Gets or sets custom string properties. /// - /// This subscript provides read-write access to custom progress properties where both the value - /// and summary types are `String`. If the property has not een set, the getter returns the - /// property's default value. + /// 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?] { @@ -403,6 +403,13 @@ extension ProgressManager { } } + /// 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 { return state.propertiesURL[MetatypeWrapper(P.self)] ?? P.self.defaultValue @@ -419,6 +426,13 @@ extension ProgressManager { } } + /// Gets or sets custom UInt64 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 UInt64 property type. public subscript(dynamicMember key: KeyPath) -> UInt64? where P.Value == UInt64, P.Summary == [UInt64] { get { return state.propertiesUInt64[MetatypeWrapper(P.self)] ?? P.self.defaultValue @@ -462,46 +476,62 @@ extension ProgressManager { // MARK: Methods to Read Additional Properties of Subtree with ProgressManager as root - /// Returns a summary for the specified integer property across the progress subtree. + /// 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: The aggregated summary value for the specified property across the entire subtree. + /// - 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 { return getUpdatedIntSummary(property: MetatypeWrapper(property)) } - /// Returns a summary for the specified double property across the progress subtree. + /// 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: The aggregated summary value for the specified property across the entire subtree. + /// - 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 { return getUpdatedDoubleSummary(property: MetatypeWrapper(property)) } - /// Returns a summary for the specified string property across the progress subtree. + /// 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 and summary types are `String`. - /// - Returns: The aggregated summary value for the specified property across the entire subtree. + /// 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?] { return getUpdatedStringSummary(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?] { return getUpdatedURLSummary(property: MetatypeWrapper(property)) } + /// Returns a summary for a custom UInt64 property across the progress subtree. + /// + /// This method aggregates the values of a custom UInt64 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 UInt64 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] { return getUpdatedUInt64Summary(property: MetatypeWrapper(property)) } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift index f5c10d2d5..fac192256 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Definitions.swift @@ -13,26 +13,25 @@ @available(FoundationPreview 6.2, *) extension ProgressManager { - /// A type that conveys task-specific information on progress. + /// 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 like completion counts and file counts. - /// - /// Properties use a key-value system where the key uniquely identifies the property type, - /// and values can be aggregated across progress manager hierarchies through reduction and merging operations. + /// 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 of individual values stored in this property. + /// The type used for individual values of this property. /// - /// This associated type represents the data type of individual property values + /// 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 data type used when summarizing property values - /// across multiple progress managers in a hierarchy. Must be `Sendable` and `Equatable`. + /// 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. @@ -54,7 +53,7 @@ extension ProgressManager { /// The default summary value for this property type. /// /// This value is used as the initial summary when no property values have been - /// aggregated yet, or as a fallback when summarization fails. + /// aggregated yet. /// /// - Returns: The default summary value for this property type. static var defaultSummary: Summary { get } @@ -80,8 +79,24 @@ extension ProgressManager { /// - Returns: A new summary that represents the combination of both input summaries. static func merge(_ summary1: Summary, _ summary2: Summary) -> Summary - /// Determines the behavior for handling childSummary when the child is deinitialized. - static func terminate(_ parentSummary: Summary, _ childSummary: 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 terminate(_ parentSummary: Summary, _ selfSummary: Summary) -> Summary } // Namespace for properties specific to operations reported on @@ -109,8 +124,8 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: Int, _ childSummary: Int) -> Int { - return parentSummary + childSummary + public static func terminate(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary } } @@ -136,8 +151,8 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: Int, _ childSummary: Int) -> Int { - return parentSummary + childSummary + public static func terminate(_ parentSummary: Int, _ selfSummary: Int) -> Int { + return parentSummary + selfSummary } } @@ -163,8 +178,8 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { - return parentSummary + childSummary + public static func terminate(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary } } @@ -190,8 +205,8 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: UInt64, _ childSummary: UInt64) -> UInt64 { - return parentSummary + childSummary + public static func terminate(_ parentSummary: UInt64, _ selfSummary: UInt64) -> UInt64 { + return parentSummary + selfSummary } } @@ -216,8 +231,8 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: [UInt64], _ childSummary: [UInt64]) -> [UInt64] { - return parentSummary + childSummary + public static func terminate(_ parentSummary: [UInt64], _ selfSummary: [UInt64]) -> [UInt64] { + return parentSummary + selfSummary } } @@ -247,7 +262,7 @@ extension ProgressManager { return max(summary1, summary2) } - public static func terminate(_ parentSummary: Duration, _ childSummary: Duration) -> Duration { + public static func terminate(_ parentSummary: Duration, _ selfSummary: Duration) -> Duration { return parentSummary } } @@ -278,7 +293,7 @@ extension ProgressManager { return summary1 + summary2 } - public static func terminate(_ parentSummary: [URL?], _ childSummary: [URL?]) -> [URL?] { + public static func terminate(_ parentSummary: [URL?], _ selfSummary: [URL?]) -> [URL?] { return parentSummary } } diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift index 845a644bc..417047459 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressReporter.swift @@ -16,7 +16,7 @@ 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. -@Observable public final class ProgressReporter: Sendable, CustomStringConvertible, CustomDebugStringConvertible { +@Observable public final class ProgressReporter: Sendable, Hashable, Equatable, CustomStringConvertible, CustomDebugStringConvertible { /// The total units of work. public var totalCount: Int? { @@ -183,4 +183,12 @@ import Observation 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) + } } From 2eaa26714a9c6b9c2af8c502ab1f6d45fa919265 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 22 Aug 2025 14:19:49 -0700 Subject: [PATCH 23/24] updated initializer --- .../ProgressManager/ProgressManager.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 3e372c513..6e3630e28 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -115,7 +115,9 @@ internal import _FoundationCollections fileURL: ProgressManager.Properties.FileURL.defaultValue, propertiesInt: [:], propertiesDouble: [:], - propertiesString: [:] + propertiesString: [:], + propertiesURL: [:], + propertiesUInt64: [:], ) self.state = Mutex(state) } From 3baa7d6e85e7f820bcc528307ea954c3013eb041 Mon Sep 17 00:00:00 2001 From: chloe-yeo Date: Fri, 22 Aug 2025 15:55:24 -0700 Subject: [PATCH 24/24] draft additional property observation fix --- .../ProgressManager+Properties+Accessors.swift | 6 ++++-- .../ProgressManager/ProgressManager.swift | 11 +++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift index d222e4cdd..6dc163477 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager+Properties+Accessors.swift @@ -541,7 +541,8 @@ extension ProgressManager { /// - Parameter property: The `TotalFileCount` property type. /// - Returns: The sum of all total file counts across the entire progress subtree. public func summary(of property: ProgressManager.Properties.TotalFileCount.Type) -> Int { - return getUpdatedFileCount(type: .total) +// return getUpdatedFileCount(type: .total) + return totalFileCount } /// Returns the completed file count across the progress subtree. @@ -549,7 +550,8 @@ extension ProgressManager { /// - Parameter property: The `CompletedFileCount` property type. /// - Returns: The sum of all completed file counts across the entire progress subtree. public func summary(of property: ProgressManager.Properties.CompletedFileCount.Type) -> Int { - return getUpdatedFileCount(type: .completed) +// return getUpdatedFileCount(type: .completed) + return completedFileCount } /// Returns the total byte count across the progress subtree. diff --git a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift index 6e3630e28..0c2401561 100644 --- a/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift +++ b/Sources/FoundationEssentials/ProgressManager/ProgressManager.swift @@ -185,6 +185,17 @@ internal import _FoundationCollections } } + //MARK: Internal vars to make additional properties observable + internal var totalFileCount: Int { + _$observationRegistrar.access(self, keyPath: \.totalFileCount) + return getUpdatedFileCount(type: .total) + } + + internal var completedFileCount: Int { + _$observationRegistrar.access(self, keyPath: \.completedFileCount) + return getUpdatedFileCount(type: .completed) + } + //MARK: Fractional Properties Methods internal func getProgressFraction() -> ProgressFraction { return state.withLock { state in