diff --git a/Package.swift b/Package.swift index 5d6a60345..a80005fb4 100644 --- a/Package.swift +++ b/Package.swift @@ -126,7 +126,8 @@ let package = Package( "CMakeLists.txt", "ProcessInfo/CMakeLists.txt", "FileManager/CMakeLists.txt", - "URL/CMakeLists.txt" + "URL/CMakeLists.txt", + "ProgressReporter/CMakeLists.txt", ], cSettings: [ .define("_GNU_SOURCE", .when(platforms: [.linux])) @@ -167,7 +168,8 @@ let package = Package( "Locale/CMakeLists.txt", "Calendar/CMakeLists.txt", "CMakeLists.txt", - "Predicate/CMakeLists.txt" + "Predicate/CMakeLists.txt", + "ProgressReporter/CMakeLists.txt", ], cSettings: wasiLibcCSettings, swiftSettings: [ diff --git a/Proposals/0024-CurrentBundle.md b/Proposals/0024-CurrentBundle.md new file mode 100644 index 000000000..9bdbdd982 --- /dev/null +++ b/Proposals/0024-CurrentBundle.md @@ -0,0 +1,184 @@ +# Introduce `#bundle` + + +* Proposal: [SF-0024](0024-filename.md) +* Authors:[Matt Seaman](https://github.com/matthewseaman), [Andreas Neusuess](https://github.com/Tantalum73) +* Review Manager: [Tina L](https://github.com/itingliu) +* Status: **Accepted** + + +## Revision history + +* **v1** Initial version +* **v1.1** Remove `#bundleDescription` and add 2 initializers to `LocalizedStringResource` + +## Introduction + +API which loads localized strings assumes `Bundle.main` by default. This works for apps, but code that runs in a framework, or was defined in a Swift package, needs to specify a different bundle. The ultimate goal is to remove this requirement in the future. One step towards that goal is to provide an easy accessor to the bundle that stores localized resources: `#bundle`. + +## Motivation + +Developers writing code in a framework or a Swift package need to repeat the `bundle` parameter for every localized string. +Without any shortcuts, loading a localized string from a framework looks like this: + +```swift +label.text = String( + localized: "She didn't clean the camera!", + bundle: Bundle(for: MyViewController.self), + comment: "Comment of astonished bystander" + ) +``` + +Because of its impracticalities, developers often write accessors to the framework's bundle: + +```swift +private class LookupClass {} +extension Bundle { + static let framework = Bundle(for: LookupClass.self) + + // Or worse yet, they lookup the bundle using its bundle identifier, which while tempting is actually rather inefficient. +} + +label.text = String( + localized: "She didn't clean the camera!", + bundle: .framework, + comment: "Comment of astonished bystander" + ) +``` + +While this solution requires less boilerplate, each framework target has to write some boilerplate still. + +In the context of a localized Swift package, the build system takes care of creating an extension on `Bundle` called `Bundle.module` at build time. While this reduces the need for boilerplate already, it makes it complicated to move code from a framework or app target into a Swift package. Each call to a localization API needs to be audited and changed to `bundle: .module`. + + +## Proposed solution and example + +We propose a macro that handles locating the right bundle with localized resources. It will work in all contexts: apps, framework targets, and Swift packages. + +```swift +label.text = String( + localized: "She didn't clean the camera!", + bundle: #bundle, + comment: "Comment of astonished bystander" + ) +``` + +## Detailed design + +We propose introducing a `#bundle` macro as follows: + +```swift +/// Returns the bundle most likely to contain resources for the calling code. +/// +/// Code in an app, app extension, framework, etc. will return the bundle associated with that target. +/// Code in a Swift Package target will return the resource bundle associated with that target. +@available(macOS 10.0, iOS 2.0, tvOS 9.0, watchOS 2.0, *) +@freestanding(expression) +public macro bundle() -> Bundle = #externalMacro(module: "FoundationMacros", type: "CurrentBundleMacro") +``` + +`#bundle` would expand to: + +```swift +{ +#if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE + return Bundle.module +#elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE + #error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.") +#else + return Bundle(_dsoHandle: #dsohandle) ?? .main +#endif +}() +``` + +This macro relies on `SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE `, a new `-D`-defined conditional that will be passed by SwiftBuild, SwiftPM, and potential 3rd party build systems under the same conditions where `Bundle.module` would be generated. + +The preprocessor macro `SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE` should be set by build systems when `Bundle.module` is not generated and the fallback `#dsohandle` approach would not retrieve the correct bundle for resources. A Swift Package without any resource files would be an example of this. Under this scenario, usage of `#bundle` presents an error. + + +It calls into new API on `Bundle`, which will be back-deployed so that using the macro isn't overly limited by the project's deployment target. + +```swift +extension Bundle { + /// Creates an instance of `Bundle` from the current value for `#dsohandle`. + /// + /// - warning: Don't call this method directly, and use `#bundle` instead. + /// + /// In the context of a Swift Package or other static library, + /// the result is the bundle that contains the produced binary, which may be + /// different from where resources are stored. + /// + /// - Parameter dsoHandle: `dsohandle` of the current binary. + @available(FoundationPreview 6.2, *) + @_alwaysEmitIntoClient + public convenience init?(_dsoHandle: UnsafeRawPointer) +``` + +The type `LocalizedStringResource` (LSR) doesn't operate on instances of `Bundle`, but `LocalizedStringResource.BundleDescription`. They can easily be converted into each other. +To make the new macro work well with LSR, we suggest adding two new initializers. We mark them as `@_alwaysEmitIntoClient` and `@_disfavoredOverload`, to avoid ambiguity over the initializers accepting a `BundleDescription` parameter: + +```swift +@available(FoundationPreview 6.2, *) +extension LocalizedStringResource { + @_alwaysEmitIntoClient + @_disfavoredOverload + public init(_ keyAndValue: String.LocalizationValue, table: String? = nil, locale: Locale = .current, bundle: Bundle, comment: StaticString? = nil) + + @_alwaysEmitIntoClient + @_disfavoredOverload + public init(_ key: StaticString, defaultValue: String.LocalizationValue, table: String? = nil, locale: Locale = .current, bundle: Bundle, comment: StaticString? = nil) +} +``` + +## Impact on existing code + +This change is purely additive. + +## Alternatives considered + +### Not using a macro + +We chose a macro because it gives us the most flexibility to update the implementation later. +This will allow us to eventually use `#bundle` (or a wrapping macro) as the default argument for the bundle parameter, which (since [SE-0422](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0422-caller-side-default-argument-macro-expression.md)) will get expanded in the caller. + +Also, only a macro lets us properly implement this for Swift Package targets since we need to either call `Bundle.module` (which only exists as a code-gen'd, internal symbol in clients) or access build-time information such as the name of the target. + +### Not doing this change + +Without this macro, developers will continue to have to write extensions on `Bundle` or repeat calling `Bundle(for: )` in their code. + + +### Using the name `#currentResourceBundle` + +Previously we discussed using the name `#currentResourceBundle` for the proposed new macro. It has been determined that `ResourceBundle` and `Bundle` describe the same thing in terms of loading resources. This macro will be used to load resources from the current bundle, repeating the fact that the current "resource bundle" is not necessary. + +### Using the name `#currentBundle` + +Previously we discussed using the name `#currentBundle` for the proposed new macro. It was pointed out that Swift already uses macros like `#filePath` or `#line`, which also imply "current". + +While `#filePath` and `#line` are unambiguous, `#bundle` could be perceived as another way to spell `Bundle.main`. Calling it `#currentBundle` would help differentiate it from `Bundle.main`. + +However, in the context of loading resources, `#bundle` is more accurate than `Bundle.main`, as it's correct in the majority of scenarios. Developers specifying `Bundle.main` when loading resources often want what `#bundle` offers, and calling the macro `#bundle` makes it easier to discover. + +We think that consistency with existing Swift macros overweighs, and that the similarity to `Bundle.main` is an advantage for discoverability. + +### Using a separate macro for `LocalizedStringResource.BundleDescription` +An earlier version of this proposal suggested to add `#bundle` and `#bundleDescription`, to work with `String(localized: ... bundle: Bundle)` and `LocalizedStringResource(... bundle: LocalizedStringResource.BundleDescription)`. + +Upon closer inspection, we can make LSR work with an instance of `Bundle` and have the proposed initializer convert it to a `LocalizedStringResource.BundleDescription` internally. This way, we only have to provide one macro, which makes it easier to discover for developers. + + +## Future Directions + +## Infer `currentBundle` by default + +This change is the first step towards not having to specify a bundle at all. Ideally, localizing a string should not require more work than using a type or method call that expresses localizability (i.e. `String.LocalizationValue`, `LocalizedStringResource`, or `String(localized: )`). + + +## Compute Package resource bundles without Bundle.module + +If we enhance `MacroExpansionContext` to include some additional information from the build system (such as target name and type), we can change the implementation of `#bundle` to compute the bundle on its own. + +This would be desirable so that the build system can inform Foundation about the bundle it creates on disk. Foundation's `#bundle` macro can ingest that information at build time, to produce code that loads the bundle in the current context. + +`Bundle.module` can't be fully removed without breaking existing code, though it could be generated as deprecated and/or gated behind a build setting. diff --git a/Sources/FoundationEssentials/CMakeLists.txt b/Sources/FoundationEssentials/CMakeLists.txt index 9c1f5b462..c1d029c14 100644 --- a/Sources/FoundationEssentials/CMakeLists.txt +++ b/Sources/FoundationEssentials/CMakeLists.txt @@ -44,6 +44,7 @@ add_subdirectory(JSON) add_subdirectory(Locale) add_subdirectory(Predicate) add_subdirectory(ProcessInfo) +add_subdirectory(ProgressReporter) add_subdirectory(PropertyList) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt new file mode 100644 index 000000000..73004105d --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/CMakeLists.txt @@ -0,0 +1,19 @@ +##===----------------------------------------------------------------------===## +## +## 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 + ProgressReporter.swift + ProgressReporter+Interop.swift + ProgressReporter+Properties.swift + Subprogress.swift) diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift new file mode 100644 index 000000000..6c5a9b19a --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressFraction.swift @@ -0,0 +1,282 @@ +//===----------------------------------------------------------------------===// +// +// 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 = 0 + 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 + } + (self.completed, self.total) = _ProgressFraction._fromDouble(double) + self.overflowed = overflow + } + + init(completed: Int, total: Int?) { + if let total { + self.total = total + self.completed = completed + } else { + self.total = 0 + 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() { + if self.total == 0 { + return + } + + (self.completed, self.total) = _ProgressFraction._simplify(completed, total) + } + + internal func simplified() -> _ProgressFraction { + let simplified = _ProgressFraction._simplify(completed, total) + return _ProgressFraction(completed: simplified.0, total: simplified.1) + } + + 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 lhs.total != 0 else { + return rhs + } + guard rhs.total != 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(lhs.total, rhs.total) { + let result = whichOverflow(lhs.completed * (lcm / lhs.total), rhs.completed * (lcm / rhs.total)) + 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() + + if let lcm = _leastCommonMultiple(lhsSimplified.total, rhsSimplified.total) { + let result = whichOverflow(lhsSimplified.completed * (lcm / lhsSimplified.total), rhsSimplified.completed * (lcm / rhsSimplified.total)) + 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: rhs.fractionCompleted * rhs.fractionCompleted, overflow: true) + } + + let newCompleted = lhs.completed.multipliedReportingOverflow(by: rhs.completed) + let newTotal = lhs.total.multipliedReportingOverflow(by: rhs.total) + + if newCompleted.overflow || newTotal.overflow { + // Try simplifying, then do it again + let lhsSimplified = lhs.simplified() + let rhsSimplified = rhs.simplified() + + let newCompletedSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.completed) + let newTotalSimplified = lhsSimplified.total.multipliedReportingOverflow(by: rhsSimplified.total) + + 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) + } + + let newTotal = lhs.total.multipliedReportingOverflow(by: rhs) + + if newTotal.overflow { + let simplified = lhs.simplified() + + let newTotalSimplified = simplified.total.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.completed == rhs.completed && lhs.total == rhs.total { + return true + } else if lhs.total == rhs.total { + // Direct comparison of numerator + return lhs.completed == rhs.completed + } 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 + let left = lhs.completed.multipliedReportingOverflow(by: rhs.total) + let right = lhs.total.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() + + let leftSimplified = lhsSimplified.completed.multipliedReportingOverflow(by: rhsSimplified.total) + let rightSimplified = lhsSimplified.total.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 { + return completed >= total && completed > 0 && total > 0 + } + + + internal var fractionCompleted : Double { + return Double(completed) / Double(total) + } + + + internal var isNaN : Bool { + return total == 0 + } + + internal var debugDescription : String { + return "\(completed) / \(total) (\(fractionCompleted))" + } + + // ---- + + 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? + // And we choose to use 131072 for N + let denominator : Int = 131072 + 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/ProgressReporter/ProgressReporter+Interop.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift new file mode 100644 index 000000000..36c12a7fd --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Interop.swift @@ -0,0 +1,117 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +@available(FoundationPreview 6.2, *) +//MARK: Progress Parent - ProgressReporter Child Interop +// Actual Progress Parent +// Ghost Progress Parent +// Ghost ProgressReporter Child +// Actual ProgressReporter Child +extension Progress { + + /// Returns a Subprogress which can be passed to any method that reports progress + /// and can be initialized into a child `ProgressReporter` to the `self`. + /// + /// Delegates a portion of totalUnitCount to a future child `ProgressReporter` instance. + /// + /// - Parameter count: Number of units delegated to a child instance of `ProgressReporter` + /// which may be instantiated by `Subprogress` later when `reporter(totalCount:)` is called. + /// - Returns: A `Subprogress` instance. + public func makeChild(withPendingUnitCount count: Int) -> Subprogress { + + // Make ghost parent & add it to actual parent's children list + let ghostProgressParent = Progress(totalUnitCount: Int64(count)) + self.addChild(ghostProgressParent, withPendingUnitCount: Int64(count)) + + // Make ghost child + let ghostReporterChild = ProgressReporter(totalCount: count) + + // Make observation instance + let observation = _ProgressParentProgressReporterChild(ghostParent: ghostProgressParent, ghostChild: ghostReporterChild) + + // Make actual child with ghost child being parent + var actualProgress = ghostReporterChild.subprogress(assigningCount: count) + actualProgress.observation = observation + actualProgress.ghostReporter = ghostReporterChild + actualProgress.interopWithProgressParent = true + return actualProgress + } +} + +private final class _ProgressParentProgressReporterChild: Sendable { + private let ghostParent: Progress + private let ghostChild: ProgressReporter + + fileprivate init(ghostParent: Progress, ghostChild: ProgressReporter) { + self.ghostParent = ghostParent + self.ghostChild = ghostChild + + // Set up mirroring observation relationship between ghostChild and ghostParent + // - Ghost Parent should mirror values from Ghost Child, and Ghost Child just mirrors values of Actual Child + ghostChild.addObserver { [weak self] observerState in + guard let self else { + return + } + + switch observerState { + case .totalCountUpdated: + self.ghostParent.totalUnitCount = Int64(self.ghostChild.totalCount ?? 0) + + case .fractionUpdated: + let count = self.ghostChild.withProperties { p in + return (p.completedCount, p.totalCount) + } + self.ghostParent.completedUnitCount = Int64(count.0) + self.ghostParent.totalUnitCount = Int64(count.1 ?? 0) + } + } + } +} + +@available(FoundationPreview 6.2, *) +//MARK: ProgressReporter Parent - Progress Child Interop +extension ProgressReporter { + + /// 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) { + let parentBridge = _NSProgressParentBridge(reporterParent: self) + progress._setParent(parentBridge, portion: Int64(count)) + + // Save ghost parent in ProgressReporter so it doesn't go out of scope after assign method ends + // So that when NSProgress increases completedUnitCount and queries for parent there is still a reference to ghostParent and parent doesn't show 0x0 (portion: 5) + self.setParentBridge(parentBridge: parentBridge) + } +} + +// Subclass of Foundation.Progress +internal final class _NSProgressParentBridge: Progress, @unchecked Sendable { + + let actualParent: ProgressReporter + + init(reporterParent: ProgressReporter) { + self.actualParent = reporterParent + super.init(parent: nil, userInfo: nil) + } + + // Overrides the _updateChild func that Foundation.Progress calls to update parent + // so that the parent that gets updated is the ProgressReporter parent + override func _updateChild(_ child: Foundation.Progress, fraction: _NSProgressFractionTuple, portion: Int64) { + actualParent.updateChildFraction(from: _ProgressFraction(nsProgressFraction: fraction.previous), to: _ProgressFraction(nsProgressFraction: fraction.next), portion: Int(portion)) + } +} +#endif diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift new file mode 100644 index 000000000..8f449cc54 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter+Properties.swift @@ -0,0 +1,58 @@ +//===----------------------------------------------------------------------===// +// +// 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 ProgressReporter { + // Namespace for properties specific to operations reported on + public struct Properties: Sendable { + public var totalFileCount: TotalFileCount.Type { TotalFileCount.self } + public struct TotalFileCount: Sendable, Property { + public static var defaultValue: Int { return 0 } + + public typealias T = Int + } + + public var completedFileCount: CompletedFileCount.Type { CompletedFileCount.self } + public struct CompletedFileCount: Sendable, Property { + public static var defaultValue: Int { return 0 } + + public typealias T = Int + } + + public var totalByteCount: TotalByteCount.Type { TotalByteCount.self } + public struct TotalByteCount: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + + public typealias T = Int64 + } + + public var completedByteCount: CompletedByteCount.Type { CompletedByteCount.self } + public struct CompletedByteCount: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + + public typealias T = Int64 + } + + public var throughput: Throughput.Type { Throughput.self } + public struct Throughput: Sendable, Property { + public static var defaultValue: Int64 { return 0 } + + public typealias T = Int64 + } + + public var estimatedTimeRemaining: EstimatedTimeRemaining.Type { EstimatedTimeRemaining.self } + public struct EstimatedTimeRemaining: Sendable, Property { + public static var defaultValue: Duration { return Duration.seconds(0) } + + public typealias T = Duration + } + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift new file mode 100644 index 000000000..f95544503 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/ProgressReporter.swift @@ -0,0 +1,492 @@ +//===----------------------------------------------------------------------===// +// +// 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 struct FractionState { + var indeterminate: Bool + var selfFraction: _ProgressFraction + var childFraction: _ProgressFraction + var overallFraction: _ProgressFraction { + selfFraction + childFraction + } + var interopChild: ProgressReporter? // read from this if self is actually an interop ghost +} + +internal struct AnyMetatypeWrapper: Hashable, Equatable, Sendable { + let metatype: Any.Type + + internal static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.metatype == rhs.metatype + } + + internal func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(metatype)) + } +} + +@available(FoundationPreview 6.2, *) +// ProgressReporter +/// An object that conveys ongoing progress to the user for a specified task. +@Observable public final class ProgressReporter: Sendable { + + // Stores all the state of properties + internal struct State { + var fractionState: FractionState + var otherProperties: [AnyMetatypeWrapper: (any Sendable)] + } + + // Interop states + internal enum ObserverState { + case fractionUpdated + case totalCountUpdated + } + + // Interop properties - Just kept alive + internal let interopObservation: (any Sendable)? // set at init + #if FOUNDATION_FRAMEWORK + internal let parentBridge: LockedState = LockedState(initialState: nil) // dummy, set upon calling setParentBridge + #endif + // Interop properties - Actually set and called + internal let ghostReporter: ProgressReporter? // set at init, used to call notify observers + internal let observers: LockedState<[@Sendable (ObserverState) -> Void]> = LockedState(initialState: [])// storage for all observers, set upon calling addObservers + + /// The total units of work. + public var totalCount: Int? { + _$observationRegistrar.access(self, keyPath: \.totalCount) + return state.withLock { state in + getTotalCount(fractionState: &state.fractionState) + } + } + + /// 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 + getCompletedCount(fractionState: &state.fractionState) + } + } + + /// 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 + getFractionCompleted(fractionState: &state.fractionState) + } + } + + /// 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 + getIsIndeterminate(fractionState: &state.fractionState) + } + } + + /// 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 + getIsFinished(fractionState: &state.fractionState) + } + } + + /// A type that conveys task-specific information on progress. + public protocol Property { + + associatedtype T: Sendable + + static var defaultValue: T { get } + + /// Aggregates an array of `T` into a single value `T`. + /// - Parameter all: Array of `T` to be aggregated. + /// - Returns: A new instance of `T`. + static func reduce(_ all: [T]) -> T + } + + /// A container that holds values for properties that specify information on progress. + @dynamicMemberLookup + public struct Values : Sendable { + //TODO: rdar://149225947 Non-escapable conformance + let reporter: ProgressReporter + var state: State + + /// The total units of work. + public var totalCount: Int? { + mutating get { + reporter.getTotalCount(fractionState: &state.fractionState) + } + + set { + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newValue && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newValue ?? 1) + } + state.fractionState.selfFraction.total = newValue ?? 0 + + // if newValue is nil, reset indeterminate to true + if newValue != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + //TODO: rdar://149015734 Check throttling + reporter.updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + reporter.ghostReporter?.notifyObservers(with: .totalCountUpdated) + + } + } + + + /// The completed units of work. + public var completedCount: Int { + mutating get { + reporter.getCompletedCount(fractionState: &state.fractionState) + } + + set { + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed = newValue + reporter.updateFractionCompleted(from: prev, to: state.fractionState.overallFraction) + reporter.ghostReporter?.notifyObservers(with: .fractionUpdated) + } + } + + /// Returns a property value that a key path indicates. + public subscript(dynamicMember key: KeyPath) -> P.T { + get { + if let val = state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] as? P.T { + return val + } else { + return P.defaultValue + } + } + + set { + state.otherProperties[AnyMetatypeWrapper(metatype: P.self)] = newValue + // Update Parent's P.self value + updateParentOtherPropertiesEntry(of: reporter, metatype: P.self, updatedValue: newValue, state: &state) + } + } + + private func updateParentOtherPropertiesEntry(of reporter: ProgressReporter, metatype: P.Type, updatedValue: P.T?, state: inout State) { + // Check if parent exists to continue propagating values up + if let parent = reporter.parent { + parent.children.withLock { children in + // Array containing all children's values to pass into reduce + let childrenValues: LockedState<[P.T]> = LockedState(initialState: []) + // Add self's updatedValue to array + if let updatedValue = updatedValue { + childrenValues.withLock { values in + values.append(updatedValue) + } + } + // Add other children's values to array, skip over existing child's existing value + for child in children { + if child != reporter { + let childValue = child?.state.withLock { $0.otherProperties[AnyMetatypeWrapper(metatype: metatype)] as? P.T } + if let childValue = childValue { + childrenValues.withLock { values in + values.append(childValue) + } + } + } + } + if !childrenValues.withLock(\.self).isEmpty { + parent.state.withLock { state in + // Set property in parent + let reducedValue = metatype.reduce(childrenValues.withLock(\.self)) + state.otherProperties[AnyMetatypeWrapper(metatype: metatype)] = reducedValue + // Recursive call to parent's parent + updateParentOtherPropertiesEntry(of: parent, metatype: metatype, updatedValue: reducedValue, state: &state) + } + } + } + } + } + + } + + private let portionOfParent: Int + internal let parent: ProgressReporter? + private let children: LockedState> + private let state: LockedState + + internal init(total: Int?, parent: ProgressReporter?, portionOfParent: Int, ghostReporter: ProgressReporter?, interopObservation: (any Sendable)?) { + self.portionOfParent = portionOfParent + self.parent = parent + self.children = .init(initialState: []) + let fractionState = FractionState( + indeterminate: total == nil ? true : false, + selfFraction: _ProgressFraction(completed: 0, total: total ?? 0), + childFraction: _ProgressFraction(completed: 0, total: 1), + interopChild: nil + ) + let state = State(fractionState: fractionState, otherProperties: [:]) + self.state = LockedState(initialState: state) + self.interopObservation = interopObservation + self.ghostReporter = ghostReporter + } + + /// Initializes `self` with `totalCount`. + /// + /// If `totalCount` is set to `nil`, `self` is indeterminate. + /// - Parameter totalCount: Total units of work. + public convenience init(totalCount: Int?) { + self.init(total: totalCount, parent: nil, portionOfParent: 0, ghostReporter: nil, interopObservation: nil) + } + + + /// Sets `totalCount`. + /// - Parameter newTotal: Total units of work. + public func setTotalCount(_ newTotal: Int?) { + state.withLock { state in + let previous = state.fractionState.overallFraction + if state.fractionState.selfFraction.total != newTotal && state.fractionState.selfFraction.total > 0 { + state.fractionState.childFraction = state.fractionState.childFraction * _ProgressFraction(completed: state.fractionState.selfFraction.total, total: newTotal ?? 1) + } + state.fractionState.selfFraction.total = newTotal ?? 0 + + // if newValue is nil, reset indeterminate to true + if newTotal != nil { + state.fractionState.indeterminate = false + } else { + state.fractionState.indeterminate = true + } + updateFractionCompleted(from: previous, to: state.fractionState.overallFraction) + + ghostReporter?.notifyObservers(with: .totalCountUpdated) + } + } + + + /// Returns a `Subprogress` representing a portion of `self`which can be passed to any method that reports progress. + /// + /// - 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 + } + + /// Increases `completedCount` by `count`. + /// - Parameter count: Units of work. + public func complete(count: Int) { + let updateState = updateCompletedCount(count: count) + updateFractionCompleted(from: updateState.previous, to: updateState.current) + ghostReporter?.notifyObservers(with: .fractionUpdated) + } + + private struct UpdateState { + let previous: _ProgressFraction + let current: _ProgressFraction + } + + private func updateCompletedCount(count: Int) -> UpdateState { + // Acquire and release child's lock + let (previous, current) = state.withLock { state in + let prev = state.fractionState.overallFraction + state.fractionState.selfFraction.completed += count + return (prev, state.fractionState.overallFraction) + } + return UpdateState(previous: previous, current: current) + } + + /// Mutates any settable properties that convey information about progress. + public func withProperties(_ closure: @Sendable (inout Values) throws -> T) rethrows -> T { + return try state.withLock { state in + var values = Values(reporter: self, state: state) + // This is done to avoid copy on write later + state = State(fractionState: FractionState(indeterminate: true, selfFraction: _ProgressFraction(), childFraction: _ProgressFraction()), otherProperties: [:]) + let result = try closure(&values) + state = values.state + return result + } + } + + //MARK: ProgressReporter Properties getters + /// Returns nil if `self` was instantiated without total units; + /// returns a `Int` value otherwise. + private func getTotalCount(fractionState: inout FractionState) -> Int? { + if let interopChild = fractionState.interopChild { + return interopChild.totalCount + } + if fractionState.indeterminate { + return nil + } else { + return fractionState.selfFraction.total + } + } + + /// Returns nil if `self` has `nil` total units; + /// returns a `Int` value otherwise. + private func getCompletedCount(fractionState: inout FractionState) -> Int { + if let interopChild = fractionState.interopChild { + return interopChild.completedCount + } + return fractionState.selfFraction.completed + } + + /// Returns 0.0 if `self` has `nil` total units; + /// returns a `Double` otherwise. + /// If `indeterminate`, return 0.0. + /// + /// The calculation of fraction completed for a ProgressReporter instance that has children + /// will take into account children's fraction completed as well. + private func getFractionCompleted(fractionState: inout FractionState) -> Double { + if let interopChild = fractionState.interopChild { + return interopChild.fractionCompleted + } + if fractionState.indeterminate { + return 0.0 + } + guard fractionState.selfFraction.total > 0 else { + return fractionState.selfFraction.fractionCompleted + } + return (fractionState.selfFraction + fractionState.childFraction).fractionCompleted + } + + + /// Returns `true` if completed and total units are not `nil` and completed units is greater than or equal to total units; + /// returns `false` otherwise. + private func getIsFinished(fractionState: inout FractionState) -> Bool { + return fractionState.selfFraction.isFinished + } + + + /// Returns `true` if `self` has `nil` total units. + private func getIsIndeterminate(fractionState: inout FractionState) -> Bool { + return fractionState.indeterminate + } + + //MARK: FractionCompleted Calculation methods + private func updateFractionCompleted(from: _ProgressFraction, to: _ProgressFraction) { + _$observationRegistrar.withMutation(of: self, keyPath: \.fractionCompleted) { + if from != to { + parent?.updateChildFraction(from: from, to: to, portion: portionOfParent) + } + } + } + + /// A child progress has been updated, which changes our own fraction completed. + internal func updateChildFraction(from previous: _ProgressFraction, to next: _ProgressFraction, portion: Int) { + // Acquire and release parent's lock + let updateState = state.withLock { state in + let previousOverallFraction = state.fractionState.overallFraction + let multiple = _ProgressFraction(completed: portion, total: state.fractionState.selfFraction.total) + let oldFractionOfParent = previous * multiple + + if previous.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction - oldFractionOfParent + } + + if next.total != 0 { + state.fractionState.childFraction = state.fractionState.childFraction + (next * multiple) + + if next.isFinished { + // Remove from children list + _ = children.withLock { $0.remove(self) } + + if portion != 0 { + // Update our self completed units + state.fractionState.selfFraction.completed += portion + } + + // Subtract the (child's fraction completed * multiple) from our child fraction + state.fractionState.childFraction = state.fractionState.childFraction - (multiple * next) + } + } + return UpdateState(previous: previousOverallFraction, current: state.fractionState.overallFraction) + } + updateFractionCompleted(from: updateState.previous, to: updateState.current) + } + + //MARK: Interop-related internal methods + /// Adds `observer` to list of `_observers` in `self`. + internal func addObserver(observer: @escaping @Sendable (ObserverState) -> Void) { + observers.withLock { observers in + observers.append(observer) + } + } + + /// Notifies all `_observers` of `self` when `state` changes. + private func notifyObservers(with state: ObserverState) { + observers.withLock { observers in + for observer in observers { + observer(state) + } + } + } + + //MARK: Internal methods to mutate locked context +#if FOUNDATION_FRAMEWORK + internal func setParentBridge(parentBridge: Foundation.Progress) { + self.parentBridge.withLock { bridge in + bridge = parentBridge + } + } +#endif + + internal func setInteropChild(interopChild: ProgressReporter) { + state.withLock { state in + state.fractionState.interopChild = interopChild + } + } + + internal func addToChildren(childReporter: ProgressReporter) { + _ = children.withLock { children in + children.insert(childReporter) + } + } +} + +@available(FoundationPreview 6.2, *) +// Default Implementation for reduce +extension ProgressReporter.Property where T : AdditiveArithmetic { + public static func reduce(_ all: [T]) -> T { + precondition(all.isEmpty == false, "Cannot reduce an empty array") + let first = all.first! + let rest = all.dropFirst() + guard !rest.isEmpty else { + return first + } + return rest.reduce(first, +) + } +} + +@available(FoundationPreview 6.2, *) +// Hashable & Equatable Conformance +extension ProgressReporter: 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: ProgressReporter, rhs: ProgressReporter) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter: CustomDebugStringConvertible { + /// The description for `completedCount` and `totalCount`. + public var debugDescription: String { + return "\(completedCount) / \(totalCount ?? 0)" + } +} diff --git a/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift new file mode 100644 index 000000000..6e8b1f540 --- /dev/null +++ b/Sources/FoundationEssentials/ProgressReporter/Subprogress.swift @@ -0,0 +1,54 @@ +//===----------------------------------------------------------------------===// +// +// 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 ProgressReporter. +/// +/// Subprogress is returned from a call to `subprogress(assigningCount:)` by a parent ProgressReporter. +/// A child ProgressReporter is then returned by calling`reporter(totalCount:)` on a Subprogress. +public struct Subprogress: ~Copyable, Sendable { + internal var parent: ProgressReporter + internal var portionOfParent: Int + internal var isInitializedToProgressReporter: Bool + + // Interop variables for Progress - ProgressReporter Interop + internal var interopWithProgressParent: Bool = false + // To be kept alive in ProgressReporter + internal var observation: (any Sendable)? + internal var ghostReporter: ProgressReporter? + + internal init(parent: ProgressReporter, portionOfParent: Int) { + self.parent = parent + self.portionOfParent = portionOfParent + self.isInitializedToProgressReporter = false + } + + /// Instantiates a ProgressReporter which is a child to the parent from which `self` is returned. + /// - Parameter totalCount: Total count of returned child `ProgressReporter` instance. + /// - Returns: A `ProgressReporter` instance. + public consuming func reporter(totalCount: Int?) -> ProgressReporter { + isInitializedToProgressReporter = true + + let childReporter = ProgressReporter(total: totalCount, parent: parent, portionOfParent: portionOfParent, ghostReporter: ghostReporter, interopObservation: observation) + + if interopWithProgressParent { + // Set interop child of ghost reporter so ghost reporter reads from here + ghostReporter?.setInteropChild(interopChild: childReporter) + } else { + // Add child to parent's _children list + parent.addToChildren(childReporter: childReporter) + } + + return childReporter + } +} + diff --git a/Sources/FoundationInternationalization/CMakeLists.txt b/Sources/FoundationInternationalization/CMakeLists.txt index baaf74513..a29953e07 100644 --- a/Sources/FoundationInternationalization/CMakeLists.txt +++ b/Sources/FoundationInternationalization/CMakeLists.txt @@ -25,6 +25,7 @@ add_subdirectory(Formatting) add_subdirectory(ICU) add_subdirectory(Locale) add_subdirectory(Predicate) +add_subdirectory(ProgressReporter) add_subdirectory(String) add_subdirectory(TimeZone) diff --git a/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt b/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt new file mode 100644 index 000000000..5cc2cc948 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressReporter/CMakeLists.txt @@ -0,0 +1,18 @@ +##===----------------------------------------------------------------------===## +## +## 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_include_directories(FoundationInternationalization PRIVATE .) +target_sources(FoundationInternationalization PRIVATE + ProgressReporter+FileFormatStyle.swift + ProgressReporter+FormatStyle.swift) diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift new file mode 100644 index 000000000..3ccf75870 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FileFormatStyle.swift @@ -0,0 +1,163 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif + +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + //TODO: rdar://149092406 Manual Codable Conformance + public struct FileFormatStyle: Sendable, Codable, Equatable, Hashable { + + internal struct Option: Sendable, Codable, Equatable, Hashable { + + enum CodingKeys: String, CodingKey { + case rawOption + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + rawOption = try container.decode(RawOption.self, forKey: .rawOption) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawOption, forKey: .rawOption) + } + + internal static var file: Option { Option(.file) } + + fileprivate enum RawOption: Codable, Equatable, Hashable { + case file + } + + fileprivate var rawOption: RawOption + + private init(_ rawOption: RawOption) { + self.rawOption = rawOption + } + } + + struct CodableRepresentation: Codable { + let locale: Locale + let includeFileDescription: Bool + } + + var codableRepresentation: CodableRepresentation { + .init(locale: self.locale, includeFileDescription: option.rawOption == .file) + } + + public init(from decoder: any Decoder) throws { + //TODO: Fix this later, codableRepresentation is not settable + let container = try decoder.singleValueContainer() + let rep = try container.decode(CodableRepresentation.self) + locale = rep.locale + option = .file + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(codableRepresentation) + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FileFormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FileFormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + + case .file: + #if FOUNDATION_FRAMEWORK + var fileCountLSR: LocalizedStringResource? + var byteCountLSR: LocalizedStringResource? + var throughputLSR: LocalizedStringResource? + var timeRemainingLSR: LocalizedStringResource? + + let properties = reporter.withProperties(\.self) + + fileCountLSR = LocalizedStringResource("\(properties.completedFileCount, format: IntegerFormatStyle()) of \(properties.totalFileCount, format: IntegerFormatStyle()) files", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + + byteCountLSR = LocalizedStringResource("\(properties.completedByteCount, format: ByteCountFormatStyle()) of \(properties.totalByteCount, format: ByteCountFormatStyle())", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + + throughputLSR = LocalizedStringResource("\(properties.throughput, format: ByteCountFormatStyle())/s", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + timeRemainingLSR = LocalizedStringResource("\(properties.estimatedTimeRemaining, format: Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide)) remaining", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + + return """ + \(String(localized: fileCountLSR ?? "")) + \(String(localized: byteCountLSR ?? "")) + \(String(localized: throughputLSR ?? "")) + \(String(localized: timeRemainingLSR ?? "")) + """ + #else + + var fileCountString: String? + var byteCountString: String? + var throughputString: String? + var timeRemainingString: String? + + let properties = reporter.withProperties(\.self) + + + fileCountString = "\(properties.completedFileCount.formatted(IntegerFormatStyle(locale: self.locale))) / \(properties.totalFileCount.formatted(IntegerFormatStyle(locale: self.locale)))" + + + byteCountString = "\(properties.completedByteCount.formatted(ByteCountFormatStyle(locale: self.locale))) / \(properties.totalByteCount.formatted(ByteCountFormatStyle(locale: self.locale)))" + + throughputString = "\(properties.throughput.formatted(ByteCountFormatStyle(locale: self.locale)))/s" + + var formatStyle = Duration.UnitsFormatStyle(allowedUnits: [.hours, .minutes], width: .wide) + formatStyle.locale = self.locale + timeRemainingString = "\(properties.estimatedTimeRemaining.formatted(formatStyle)) remaining" + + return """ + \(fileCountString ?? "") + \(byteCountString ?? "") + \(throughputString ?? "") + \(timeRemainingString ?? "") + """ + #endif + } + } +} + +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + public func formatted(_ style: ProgressReporter.FileFormatStyle) -> String { + style.format(self) + } +} + +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FileFormatStyle { + public static var file: Self { + .init(.file) + } +} diff --git a/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift new file mode 100644 index 000000000..9f83d5f59 --- /dev/null +++ b/Sources/FoundationInternationalization/ProgressReporter/ProgressReporter+FormatStyle.swift @@ -0,0 +1,162 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// +#if canImport(FoundationEssentials) +import FoundationEssentials +#endif +// Outlines the options available to format ProgressReporter +@available(FoundationPreview 6.2, *) +extension ProgressReporter { + + public struct FormatStyle: Sendable, Codable, Equatable, Hashable { + + // Outlines the options available to format ProgressReporter + internal struct Option: Sendable, Codable, Hashable, Equatable { + + enum CodingKeys: String, CodingKey { + case rawOption + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + rawOption = try container.decode(RawOption.self, forKey: .rawOption) + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(rawOption, forKey: .rawOption) + } + + /// Option specifying `fractionCompleted`. + /// + /// For example, 20% completed. + /// - Parameter style: A `FloatingPointFormatStyle.Percent` instance that should be used to format `fractionCompleted`. + /// - Returns: A `LocalizedStringResource` for formatted `fractionCompleted`. + internal static func fractionCompleted(format style: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Option { + return Option(.fractionCompleted(style)) + } + + /// Option specifying `completedCount` / `totalCount`. + /// + /// For example, 5 of 10. + /// - Parameter style: An `IntegerFormatStyle` instance that should be used to format `completedCount` and `totalCount`. + /// - Returns: A `LocalizedStringResource` for formatted `completedCount` / `totalCount`. + internal static func count(format style: IntegerFormatStyle = IntegerFormatStyle() + ) -> Option { + return Option(.count(style)) + } + + fileprivate enum RawOption: Codable, Hashable, Equatable { + case count(IntegerFormatStyle) + case fractionCompleted(FloatingPointFormatStyle.Percent) + } + + fileprivate var rawOption: RawOption + + private init(_ rawOption: RawOption) { + self.rawOption = rawOption + } + } + + enum CodingKeys: String, CodingKey { + case locale + case option + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + locale = try container.decode(Locale.self, forKey: .locale) + option = try container.decode(Option.self, forKey: .option) + } + + public func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(locale, forKey: .locale) + try container.encode(option, forKey: .option) + } + + public var locale: Locale + let option: Option + + internal init(_ option: Option, locale: Locale = .autoupdatingCurrent) { + self.locale = locale + self.option = option + } + } +} + +@available(FoundationPreview 6.2, *) +extension ProgressReporter.FormatStyle: FormatStyle { + + public func locale(_ locale: Locale) -> ProgressReporter.FormatStyle { + .init(self.option, locale: locale) + } + + public func format(_ reporter: ProgressReporter) -> String { + switch self.option.rawOption { + case .count(let countStyle): + let count = reporter.withProperties { p in + return (p.completedCount, p.totalCount) + } + #if FOUNDATION_FRAMEWORK + let countLSR = LocalizedStringResource("\(count.0, format: countStyle) of \(count.1 ?? 0, format: countStyle)", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: countLSR) + #else + return "\(count.0.formatted(countStyle.locale(self.locale))) / \((count.1 ?? 0).formatted(countStyle.locale(self.locale)))" + #endif + + case .fractionCompleted(let fractionStyle): + #if FOUNDATION_FRAMEWORK + let fractionLSR = LocalizedStringResource("\(reporter.fractionCompleted, format: fractionStyle) completed", locale: self.locale, bundle: .forClass(ProgressReporter.self)) + return String(localized: fractionLSR) + #else + return "\(reporter.fractionCompleted.formatted(fractionStyle.locale(self.locale)))" + #endif + } + } +} + +@available(FoundationPreview 6.2, *) +// Make access easier to format ProgressReporter +extension ProgressReporter { + +#if FOUNDATION_FRAMEWORK + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } +#else + public func formatted(_ style: F) -> F.FormatOutput where F.FormatInput == ProgressReporter { + style.format(self) + } +#endif // FOUNDATION_FRAMEWORK + + public func formatted() -> String { + self.formatted(.fractionCompleted()) + } + +} + +@available(FoundationPreview 6.2, *) +extension FormatStyle where Self == ProgressReporter.FormatStyle { + + public static func fractionCompleted( + format: FloatingPointFormatStyle.Percent = FloatingPointFormatStyle.Percent() + ) -> Self { + .init(.fractionCompleted(format: format)) + } + + public static func count( + format: IntegerFormatStyle = IntegerFormatStyle() + ) -> Self { + .init(.count(format: format)) + } +} diff --git a/Sources/FoundationMacros/BundleMacro.swift b/Sources/FoundationMacros/BundleMacro.swift new file mode 100644 index 000000000..bd45c8ca4 --- /dev/null +++ b/Sources/FoundationMacros/BundleMacro.swift @@ -0,0 +1,30 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftSyntax +import SwiftSyntaxMacros + +public struct BundleMacro: SwiftSyntaxMacros.ExpressionMacro, Sendable { + public static func expansion(of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext) throws -> ExprSyntax { + """ + { + #if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE + return Bundle.module + #elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE + #error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.") + #else + return Bundle(_dsoHandle: #dsohandle) ?? .main + #endif + }() + """ + } +} diff --git a/Sources/FoundationMacros/CMakeLists.txt b/Sources/FoundationMacros/CMakeLists.txt index 98b90c0df..2cd1a338e 100644 --- a/Sources/FoundationMacros/CMakeLists.txt +++ b/Sources/FoundationMacros/CMakeLists.txt @@ -63,6 +63,7 @@ target_compile_options(FoundationMacros PRIVATE -parse-as-library) target_sources(FoundationMacros PRIVATE FoundationMacros.swift + BundleMacro.swift PredicateMacro.swift) target_compile_options(FoundationMacros PRIVATE diff --git a/Sources/FoundationMacros/FoundationMacros.swift b/Sources/FoundationMacros/FoundationMacros.swift index dedf6f386..78e6696f1 100644 --- a/Sources/FoundationMacros/FoundationMacros.swift +++ b/Sources/FoundationMacros/FoundationMacros.swift @@ -17,7 +17,11 @@ import SwiftCompilerPlugin @main struct FoundationMacros: CompilerPlugin { - var providingMacros: [Macro.Type] = [PredicateMacro.self, ExpressionMacro.self] + var providingMacros: [Macro.Type] = [ + PredicateMacro.self, + ExpressionMacro.self, + BundleMacro.self + ] } #endif diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift new file mode 100644 index 000000000..89dc77ec7 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressFractionTests.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +final class ProgressFractionTests: XCTestCase { + func test_equal() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 100, total: 200) + + XCTAssertEqual(f1, f2) + + let f3 = _ProgressFraction(completed: 3, total: 10) + XCTAssertNotEqual(f1, f3) + + let f4 = _ProgressFraction(completed: 5, total: 10) + XCTAssertEqual(f1, f4) + } + + func test_addSame() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = f1 + f2 + XCTAssertEqual(r.completed, 8) + XCTAssertEqual(r.total, 10) + } + + func test_addDifferent() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 300, total: 1000) + + let r = f1 + f2 + XCTAssertEqual(r.completed, 800) + XCTAssertEqual(r.total, 1000) + } + + func test_subtract() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = f1 - f2 + XCTAssertEqual(r.completed, 2) + XCTAssertEqual(r.total, 10) + } + + func test_multiply() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 1, total: 2) + + let r = f1 * f2 + XCTAssertEqual(r.completed, 5) + XCTAssertEqual(r.total, 20) + } + + func test_simplify() { + let f1 = _ProgressFraction(completed: 5, total: 10) + let f2 = _ProgressFraction(completed: 3, total: 10) + + let r = (f1 + f2).simplified() + + XCTAssertEqual(r.completed, 4) + XCTAssertEqual(r.total, 5) + } + + func test_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) + } + + XCTAssertEqual(fractionResult, expectedResult, accuracy: 0.00001) + } + + func test_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 + XCTAssertTrue(f1.overflowed) + + let f2 = _ProgressFraction(completed: 1, total: 4) + f1 + + // f2 should also be in overflow + XCTAssertTrue(f2.overflowed) + + // And it should have completed value of about 1.0/4.0 + f1.fractionCompleted + let expected = (1.0 / 4.0) + f1.fractionCompleted + + XCTAssertEqual(expected, f2.fractionCompleted, accuracy: 0.00001) + } + +#if _pointerBitWidth(_64) // These tests assumes Int is Int64 + func test_andAndSubtractOverflow() { + 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 + XCTAssertTrue(result1.completed > 0) + + let result2 = (f3 - f2) + f1 + XCTAssertTrue(result2.completed < 60) + } +#endif + + func test_fractionFromDouble() { + let d = 4.25 // exactly representable in binary + let f1 = _ProgressFraction(double: d) + + let simplified = f1.simplified() + XCTAssertEqual(simplified.completed, 17) + XCTAssertEqual(simplified.total, 4) + } + + func test_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 + XCTAssertFalse(r.overflowed) + } +} diff --git a/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift new file mode 100644 index 000000000..a6c21acb1 --- /dev/null +++ b/Tests/FoundationEssentialsTests/ProgressReporter/ProgressReporterTests.swift @@ -0,0 +1,471 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest + +#if FOUNDATION_FRAMEWORK +@testable import Foundation +#else +@testable import FoundationEssentials +#endif // FOUNDATION_FRAMEWORK + +/// Unit tests for basic functionalities of ProgressReporter +class TestProgressReporter: XCTestCase { + /// MARK: Helper methods that report progress + func doBasicOperationV1(reportTo progress: consuming Subprogress) async { + let reporter = progress.reporter(totalCount: 8) + for i in 1...8 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(8)) + } + } + + func doBasicOperationV2(reportTo progress: consuming Subprogress) async { + let reporter = progress.reporter(totalCount: 7) + for i in 1...7 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted,Double(i) / Double(7)) + } + } + + func doBasicOperationV3(reportTo progress: consuming Subprogress) async { + let reporter = progress.reporter(totalCount: 11) + for i in 1...11 { + reporter.complete(count: 1) + XCTAssertEqual(reporter.completedCount, i) + XCTAssertEqual(reporter.fractionCompleted, Double(i) / Double(11)) + } + } + + func doFileOperation(reportTo progress: consuming Subprogress) async { + let reporter = progress.reporter(totalCount: 100) + reporter.withProperties { properties in + properties.totalFileCount = 100 + } + + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + + reporter.complete(count: 100) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertTrue(reporter.isFinished) + + reporter.withProperties { properties in + properties.completedFileCount = 100 + } + XCTAssertEqual(reporter.withProperties(\.completedFileCount), 100) + XCTAssertEqual(reporter.withProperties(\.totalFileCount), 100) + } + + /// MARK: Tests calculations based on change in totalCount + func testTotalCountNil() async throws { + let overall = ProgressReporter(totalCount: nil) + overall.complete(count: 10) + XCTAssertEqual(overall.completedCount, 10) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertNil(overall.totalCount) + } + + func testTotalCountReset() async throws { + let overall = ProgressReporter(totalCount: 10) + overall.complete(count: 5) + XCTAssertEqual(overall.completedCount, 5) + XCTAssertEqual(overall.totalCount, 10) + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertFalse(overall.isIndeterminate) + + overall.withProperties { p in + p.totalCount = nil + p.completedCount += 1 + } + XCTAssertEqual(overall.completedCount, 6) + XCTAssertNil(overall.totalCount) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + overall.withProperties { p in + p.totalCount = 12 + p.completedCount += 2 + } + XCTAssertEqual(overall.completedCount, 8) + XCTAssertEqual(overall.totalCount, 12) + XCTAssertEqual(overall.fractionCompleted, Double(8) / Double(12)) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + } + + func testTotalCountNilWithChild() async throws { + let overall = ProgressReporter(totalCount: nil) + XCTAssertEqual(overall.completedCount, 0) + XCTAssertNil(overall.totalCount) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + let progress1 = overall.subprogress(assigningCount: 2) + let reporter1 = progress1.reporter(totalCount: 1) + + reporter1.complete(count: 1) + XCTAssertEqual(reporter1.totalCount, 1) + XCTAssertEqual(reporter1.completedCount, 1) + XCTAssertEqual(reporter1.fractionCompleted, 1.0) + XCTAssertFalse(reporter1.isIndeterminate) + XCTAssertTrue(reporter1.isFinished) + + XCTAssertEqual(overall.completedCount, 2) + XCTAssertEqual(overall.totalCount, nil) + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertTrue(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + + overall.withProperties { p in + p.totalCount = 5 + } + XCTAssertEqual(overall.completedCount, 2) + XCTAssertEqual(overall.totalCount, 5) + XCTAssertEqual(overall.fractionCompleted, 0.4) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertFalse(overall.isFinished) + } + + func testTotalCountFinishesWithLessCompletedCount() async throws { + let overall = ProgressReporter(totalCount: 10) + overall.complete(count: 5) + + let progress1 = overall.subprogress(assigningCount: 8) + let reporter1 = progress1.reporter(totalCount: 1) + reporter1.complete(count: 1) + + XCTAssertEqual(overall.completedCount, 13) + XCTAssertEqual(overall.totalCount, 10) + XCTAssertEqual(overall.fractionCompleted, 1.3) + XCTAssertFalse(overall.isIndeterminate) + XCTAssertTrue(overall.isFinished) + } + + /// MARK: Tests single-level tree + func testDiscreteReporter() async throws { + let reporter = ProgressReporter(totalCount: 3) + await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 3)) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertEqual(reporter.completedCount, 3) + XCTAssertTrue(reporter.isFinished) + } + + func testDiscreteReporterWithFileProperties() async throws { + let fileReporter = ProgressReporter(totalCount: 3) + await doFileOperation(reportTo: fileReporter.subprogress(assigningCount: 3)) + XCTAssertEqual(fileReporter.fractionCompleted, 1.0) + XCTAssertEqual(fileReporter.completedCount, 3) + XCTAssertTrue(fileReporter.isFinished) + } + + /// MARK: Tests multiple-level trees + func testEmptyDiscreteReporter() async throws { + let reporter = ProgressReporter(totalCount: nil) + XCTAssertTrue(reporter.isIndeterminate) + + reporter.withProperties { p in + p.totalCount = 10 + } + XCTAssertFalse(reporter.isIndeterminate) + XCTAssertEqual(reporter.totalCount, 10) + + await doBasicOperationV1(reportTo: reporter.subprogress(assigningCount: 10)) + XCTAssertEqual(reporter.fractionCompleted, 1.0) + XCTAssertEqual(reporter.completedCount, 10) + XCTAssertTrue(reporter.isFinished) + } + + func testTwoLevelTreeWithOneChildWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 10) + reporter1.withProperties { properties in + properties.totalFileCount = 10 + properties.completedFileCount = 0 + } + reporter1.complete(count: 10) + + XCTAssertEqual(overall.fractionCompleted, 0.5) + // This should call reduce and get 10 + XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) + } + + func testTwoLevelTreeWithTwoChildrenWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 10) + + reporter1.withProperties { properties in + properties.totalFileCount = 11 + properties.completedFileCount = 0 + } + + let progress2 = overall.subprogress(assigningCount: 1) + let reporter2 = progress2.reporter(totalCount: 10) + + reporter2.withProperties { properties in + properties.totalFileCount = 9 + properties.completedFileCount = 0 + } + + XCTAssertEqual(overall.fractionCompleted, 0.0) + XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) + + // Update FileCounts + reporter1.withProperties { properties in + properties.completedFileCount = 1 + } + + reporter2.withProperties { properties in + properties.completedFileCount = 1 + } + + XCTAssertEqual(overall.withProperties(\.completedFileCount), 2) + } + + func testThreeLevelTreeWithFileProperties() async throws { + let overall = ProgressReporter(totalCount: 1) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 5) + + let childProgress1 = reporter1.subprogress(assigningCount: 3) + let childReporter1 = childProgress1.reporter(totalCount: nil) + childReporter1.withProperties { properties in + properties.totalFileCount += 10 + } + + let childProgress2 = reporter1.subprogress(assigningCount: 2) + let childReporter2 = childProgress2.reporter(totalCount: nil) + childReporter2.withProperties { properties in + properties.totalFileCount += 10 + } + + XCTAssertEqual(reporter1.withProperties(\.totalFileCount), 20) + + // Tests that totalFileCount propagates to root level + XCTAssertEqual(overall.withProperties(\.totalFileCount), 20) + } + + func testTwoLevelTreeWithTwoChildren() async throws { + let overall = ProgressReporter(totalCount: 2) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount: 1)) + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedCount, 1) + XCTAssertFalse(overall.isFinished) + XCTAssertFalse(overall.isIndeterminate) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount: 1)) + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedCount, 2) + XCTAssertTrue(overall.isFinished) + XCTAssertFalse(overall.isIndeterminate) + } + + func testTwoLevelTreeWithTwoChildrenWithOneFileProperty() async throws { + let overall = ProgressReporter(totalCount: 2) + + let progress1 = overall.subprogress(assigningCount: 1) + let reporter1 = progress1.reporter(totalCount: 5) + reporter1.complete(count: 5) + + let progress2 = overall.subprogress(assigningCount: 1) + let reporter2 = progress2.reporter(totalCount: 5) + reporter2.withProperties { properties in + properties.totalFileCount = 10 + } + + XCTAssertEqual(overall.fractionCompleted, 0.5) + // Parent is expected to get totalFileCount from one of the children with a totalFileCount + XCTAssertEqual(overall.withProperties(\.totalFileCount), 10) + } + + func testTwoLevelTreeWithMultipleChildren() async throws { + let overall = ProgressReporter(totalCount: 3) + + await doBasicOperationV1(reportTo: overall.subprogress(assigningCount:1)) + XCTAssertEqual(overall.fractionCompleted, Double(1) / Double(3)) + XCTAssertEqual(overall.completedCount, 1) + + await doBasicOperationV2(reportTo: overall.subprogress(assigningCount:1)) + XCTAssertEqual(overall.fractionCompleted, Double(2) / Double(3)) + XCTAssertEqual(overall.completedCount, 2) + + await doBasicOperationV3(reportTo: overall.subprogress(assigningCount:1)) + XCTAssertEqual(overall.fractionCompleted, Double(3) / Double(3)) + XCTAssertEqual(overall.completedCount, 3) + } + + func testThreeLevelTree() async throws { + let overall = ProgressReporter(totalCount: 100) + XCTAssertEqual(overall.fractionCompleted, 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let reporter1 = child1.reporter(totalCount: 100) + + let grandchild1 = reporter1.subprogress(assigningCount: 100) + let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + + XCTAssertEqual(overall.fractionCompleted, 0.0) + + grandchildReporter1.complete(count: 50) + XCTAssertEqual(reporter1.fractionCompleted, 0.5) + XCTAssertEqual(overall.fractionCompleted, 0.5) + + grandchildReporter1.complete(count: 50) + XCTAssertEqual(reporter1.fractionCompleted, 1.0) + XCTAssertEqual(overall.fractionCompleted, 1.0) + + XCTAssertTrue(grandchildReporter1.isFinished) + XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(overall.isFinished) + } + + func testFourLevelTree() async throws { + let overall = ProgressReporter(totalCount: 100) + XCTAssertEqual(overall.fractionCompleted, 0.0) + + let child1 = overall.subprogress(assigningCount: 100) + let reporter1 = child1.reporter(totalCount: 100) + + let grandchild1 = reporter1.subprogress(assigningCount: 100) + let grandchildReporter1 = grandchild1.reporter(totalCount: 100) + + XCTAssertEqual(overall.fractionCompleted, 0.0) + + + let greatGrandchild1 = grandchildReporter1.subprogress(assigningCount: 100) + let greatGrandchildReporter1 = greatGrandchild1.reporter(totalCount: 100) + + greatGrandchildReporter1.complete(count: 50) + XCTAssertEqual(overall.fractionCompleted, 0.5) + + greatGrandchildReporter1.complete(count: 50) + XCTAssertEqual(overall.fractionCompleted, 1.0) + + XCTAssertTrue(greatGrandchildReporter1.isFinished) + XCTAssertTrue(grandchildReporter1.isFinished) + XCTAssertTrue(reporter1.isFinished) + XCTAssertTrue(overall.isFinished) + } +} + +#if FOUNDATION_FRAMEWORK +/// Unit tests for interop methods that support building Progress trees with both Progress and ProgressReporter +class TestProgressReporterInterop: XCTestCase { + func doSomethingWithProgress(expectation1: XCTestExpectation, expectation2: XCTestExpectation) async -> Progress { + let p = Progress(totalUnitCount: 2) + Task.detached { + p.completedUnitCount = 1 + expectation1.fulfill() + p.completedUnitCount = 2 + expectation2.fulfill() + } + return p + } + + func doSomethingWithReporter(progress: consuming Subprogress?) async { + let reporter = progress?.reporter(totalCount: 4) + reporter?.complete(count: 2) + reporter?.complete(count: 2) + } + + func testInteropProgressParentProgressReporterChild() async throws { + // Initialize a Progress Parent + let overall = Progress.discreteProgress(totalUnitCount: 10) + + // Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p1 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overall.addChild(p1, withPendingUnitCount: 5) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if ProgressReporter values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 0.5) + XCTAssertEqual(overall.completedUnitCount, 5) + + // Add ProgressReporter as Child + let p2 = overall.makeChild(withPendingUnitCount: 5) + await doSomethingWithReporter(progress: p2) + + // Check if Progress values propagate to Progress parent + XCTAssertEqual(overall.fractionCompleted, 1.0) + XCTAssertEqual(overall.completedUnitCount, 10) + } + + func testInteropProgressReporterParentProgressChild() async throws { + // Initialize ProgressReporter parent + let overallReporter = ProgressReporter(totalCount: 10) + + // Add ProgressReporter as Child + await doSomethingWithReporter(progress: overallReporter.subprogress(assigningCount: 5)) + + // Check if ProgressReporter values propagate to ProgressReporter parent + XCTAssertEqual(overallReporter.fractionCompleted, 0.5) + XCTAssertEqual(overallReporter.completedCount, 5) + + // Interop: Add Progress as Child + let expectation1 = XCTestExpectation(description: "Set completed unit count to 1") + let expectation2 = XCTestExpectation(description: "Set completed unit count to 2") + let p2 = await doSomethingWithProgress(expectation1: expectation1, expectation2: expectation2) + overallReporter.subprogress(assigningCount: 5, to: p2) + + await fulfillment(of: [expectation1, expectation2], timeout: 10.0) + + // Check if Progress values propagate to ProgressRerpoter parent + XCTAssertEqual(overallReporter.completedCount, 10) + XCTAssertEqual(overallReporter.totalCount, 10) + XCTAssertEqual(overallReporter.fractionCompleted, 1.0) + } + + func getProgressWithTotalCountInitialized() -> Progress { + return Progress(totalUnitCount: 5) + } + + func receiveProgress(progress: consuming Subprogress) { + let _ = progress.reporter(totalCount: 5) + } + + func testInteropProgressReporterParentProgressChildConsistency() async throws { + let overallReporter = ProgressReporter(totalCount: nil) + let child = overallReporter.subprogress(assigningCount: 5) + receiveProgress(progress: child) + XCTAssertNil(overallReporter.totalCount) + + let overallReporter2 = ProgressReporter(totalCount: nil) + let interopChild = getProgressWithTotalCountInitialized() + overallReporter2.subprogress(assigningCount: 5, to: interopChild) + XCTAssertNil(overallReporter2.totalCount) + } + + func testInteropProgressParentProgressReporterChildConsistency() async throws { + let overallProgress = Progress() + let child = Progress(totalUnitCount: 5) + overallProgress.addChild(child, withPendingUnitCount: 5) + XCTAssertEqual(overallProgress.totalUnitCount, 0) + + let overallProgress2 = Progress() + let interopChild = overallProgress2.makeChild(withPendingUnitCount: 5) + receiveProgress(progress: interopChild) + XCTAssertEqual(overallProgress2.totalUnitCount, 0) + } +} +#endif diff --git a/Tests/FoundationMacrosTests/BundleMacroTests.swift b/Tests/FoundationMacrosTests/BundleMacroTests.swift new file mode 100644 index 000000000..5b466f422 --- /dev/null +++ b/Tests/FoundationMacrosTests/BundleMacroTests.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2022-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 XCTest +import FoundationMacros + +final class BundleMacroTests: XCTestCase { + + func testSimple() { + AssertMacroExpansion( + macros: ["bundle": BundleMacro.self], + """ + #bundle + """, + """ + { + #if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE + return Bundle.module + #elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE + #error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.") + #else + return Bundle(_dsoHandle: #dsohandle) ?? .main + #endif + }() + """ + ) + } + + func testUsingParenthesis() { + AssertMacroExpansion( + macros: ["bundle": BundleMacro.self], + """ + #bundle() + """, + """ + { + #if SWIFT_MODULE_RESOURCE_BUNDLE_AVAILABLE + return Bundle.module + #elseif SWIFT_MODULE_RESOURCE_BUNDLE_UNAVAILABLE + #error("No resource bundle is available for this module. If resources are included elsewhere, specify the bundle manually.") + #else + return Bundle(_dsoHandle: #dsohandle) ?? .main + #endif + }() + """ + ) + } +}