diff --git a/.jazzy.yml b/.jazzy.yml index 03040143..1aad7cdb 100644 --- a/.jazzy.yml +++ b/.jazzy.yml @@ -28,7 +28,9 @@ custom_categories: - CFRunLoop - name: Futures Than Can Fail children: + - TaskResult - Task + - TaskProtocol - Either - name: Coordinating Access to Data children: @@ -46,7 +48,7 @@ github_url: "https://github.com/bignerdranch/Deferred" head: | - + hide_documentation_coverage: true module: Deferred diff --git a/Sources/Deferred/Locking.swift b/Sources/Deferred/Locking.swift index 4afbd484..72d8ed26 100644 --- a/Sources/Deferred/Locking.swift +++ b/Sources/Deferred/Locking.swift @@ -96,7 +96,7 @@ public final class NativeLock: Locking { } } #else -public typealias NativeLock = NSLock +typealias NativeLock = NSLock #endif /// A readers-writer lock provided by the platform implementation of the diff --git a/Sources/Task/TaskChain.swift b/Sources/Task/TaskChain.swift index 078516d2..05feda42 100644 --- a/Sources/Task/TaskChain.swift +++ b/Sources/Task/TaskChain.swift @@ -30,8 +30,11 @@ struct TaskChain { /// that first task's `Root`. @objc(BNRTaskRootProgress) private class Root: Progress { + /// Key for value of type `Root?` in `Thread.threadDictionary`. static let threadKey = "_BNRTaskCurrentRoot" + /// Propogates the current Task chain for explicit composition during + /// `Task.andThen`. static var active: Root? { get { return Thread.current.threadDictionary[threadKey] as? Root @@ -40,6 +43,30 @@ struct TaskChain { Thread.current.threadDictionary[threadKey] = newValue } } + + /// Key for value of type `Bool` in `Progress.userInfo`. + static let expandsKey = ProgressUserInfoKey(rawValue: "_BNRTaskExpandChildren") + + /// If `true`, Propogates the current Task chain for explicit composition during + /// `Task.andThen`. + var expandsAddedChildren: Bool { + get { + return userInfo[Root.expandsKey] as? Bool == true + } + set { + setUserInfoObject(newValue, forKey: Root.expandsKey) + } + } + + @available(macOS 10.11, iOS 9.0, watchOS 2.0, tvOS 9.0, *) + override func addChild(_ child: Progress, withPendingUnitCount unitCount: Int64) { + if expandsAddedChildren, !child.wasGeneratedByTask { + totalUnitCount += TaskChain.explicitChildUnitCount - unitCount + super.addChild(child, withPendingUnitCount: TaskChain.explicitChildUnitCount) + } else { + super.addChild(child, withPendingUnitCount: unitCount) + } + } } /// The beginning of this chain. May be the same as `effectiveProgress`. @@ -99,16 +126,19 @@ struct TaskChain { // MARK: - - /// The handler passed to `map` supports explicit progress reporting. - /// Because we must commit to a `pendingUnitCount` in order to - /// `becomeCurrent`, for now it only attaches for a single unit. + /// The handler passed to `map` can use implicit progress reporting. + /// During the handler, the first `Progress` object created using + /// `parent: .current()` will be given a 100x slice of the task chain on + /// macOS 10.11, iOS 9, and above. func beginMap() { + root.expandsAddedChildren = true root.becomeCurrent(withPendingUnitCount: TaskChain.singleUnit) } /// See `beginMap`. func commitMap() { root.resignCurrent() + root.expandsAddedChildren = false } // MARK: - diff --git a/Tests/TaskTests/TaskProgressTests.swift b/Tests/TaskTests/TaskProgressTests.swift index 1ff2721c..aaa217f5 100644 --- a/Tests/TaskTests/TaskProgressTests.swift +++ b/Tests/TaskTests/TaskProgressTests.swift @@ -15,6 +15,8 @@ import Task import Deferred #endif +// swiftlint:disable type_body_length + #if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) class TaskProgressTests: CustomExecutorTestCase { static let allTests: [(String, (TaskProgressTests) -> () throws -> Void)] = [ @@ -308,5 +310,46 @@ class TaskProgressTests: CustomExecutorTestCase { XCTAssertEqual(task.progress.totalUnitCount, 103) XCTAssertEqual(task.progress.fractionCompleted, 1) } + + func testThatMappingWithCustomProgressIsWeighted() { + let promise1 = Task.Promise() + let expect = expectation(description: "map handler has started executing") + + let task = promise1 + .map(upon: .any(), transform: { (value) -> Int in + XCTAssertNotNil(Progress.current()) + + let customProgress = Progress(totalUnitCount: 32) + expect.fulfill() + + customProgress.completedUnitCount = 1 + customProgress.completedUnitCount = 2 + customProgress.completedUnitCount = 4 + customProgress.completedUnitCount = 8 + customProgress.completedUnitCount = 16 + customProgress.completedUnitCount = 32 + + return value * 2 + }) + .map(upon: .any(), transform: { "\($0)" }) + .map(upon: .any(), transform: { "\($0)\($0)" }) + + XCTAssertEqual(task.progress.completedUnitCount, 0) + XCTAssertEqual(task.progress.totalUnitCount, 4) + XCTAssertEqual(task.progress.fractionCompleted, 0) + + promise1.succeed(with: 9000) + + wait(for: [ expect ], timeout: shortTimeout) + + XCTAssertEqual(task.progress.totalUnitCount, 103) + + wait(for: [ + expectation(toFinish: task.progress) + ], timeout: longTimeout) + + XCTAssertEqual(task.progress.completedUnitCount, 103) + XCTAssertEqual(task.progress.totalUnitCount, 103) + } } #endif