From 3c28a5ad251de5ff349544918486e29bce100653 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 14:46:23 -0800 Subject: [PATCH 01/31] Major `Operation` and `OperationQueue` improvements, added `OperationQueueStatus` --- .../Closure/AsyncClosureOperation.swift | 11 +- .../Operation/Closure/ClosureOperation.swift | 11 +- ...=> InteractiveAsyncClosureOperation.swift} | 31 +++-- ...wift => InteractiveClosureOperation.swift} | 22 +++- .../Complex/AtomicBlockOperation.swift | 121 ++++++++++++------ .../Foundational/BasicAsyncOperation.swift | 10 +- .../Foundational/BasicOperation.swift | 39 +++++- .../Operation/Operation Extensions.swift | 19 +-- .../OperationQueue/AtomicOperationQueue.swift | 24 ++-- .../BasicOperationQueue Status.swift | 30 +++++ .../OperationQueue/BasicOperationQueue.swift | 121 +++++++++++++++++- .../Closure/AsyncClosureOperation Tests.swift | 103 ++++++++++++--- .../Closure/ClosureOperation Tests.swift | 82 ++++++++++-- ...eractiveAsyncClosureOperation Tests.swift} | 115 ++++++++++++----- ...> InteractiveClosureOperation Tests.swift} | 18 +-- .../Complex/AtomicBlockOperation Tests.swift | 67 ++++++---- .../BasicAsyncOperation Tests.swift | 94 +++++++++++--- .../Foundational/BasicOperation Tests.swift | 71 +++++++--- .../AtomicOperationQueue Tests.swift | 2 +- .../OperationQueue Extensions Tests.swift | 28 ++-- 20 files changed, 784 insertions(+), 235 deletions(-) rename Sources/OTCore/Threading/Operation/Closure/{CancellableAsyncClosureOperation.swift => InteractiveAsyncClosureOperation.swift} (72%) rename Sources/OTCore/Threading/Operation/Closure/{CancellableClosureOperation.swift => InteractiveClosureOperation.swift} (71%) create mode 100644 Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift rename Tests/OTCoreTests/Threading/Operation/Closure/{CancellableAsyncClosureOperation Tests.swift => InteractiveAsyncClosureOperation Tests.swift} (59%) rename Tests/OTCoreTests/Threading/Operation/Closure/{CancellableClosureOperation Tests.swift => InteractiveClosureOperation Tests.swift} (88%) diff --git a/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift index 5c88506..4d381d7 100644 --- a/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift +++ b/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift @@ -16,7 +16,7 @@ import Foundation /// /// No special method calls are required in the main block. /// -/// This closure is not cancellable once it is started. If you want to allow cancellation (early return partway through operation execution) use `CancellableAsyncClosureOperation` instead. +/// This closure is not cancellable once it is started, and does not offer a reference to update progress information. If you want to allow cancellation (early return partway through operation execution) or progress updating, use `InteractiveAsyncClosureOperation` instead. /// /// // if not specifying a dispatch queue, the operation will /// // run on the current thread if started manually, @@ -24,14 +24,19 @@ import Foundation /// // will be automatically managed /// let op = AsyncClosureOperation { /// // ... do some work ... +/// +/// // operation completes & cleans up automatically +/// // after closure finishes /// } /// +/// Execution on a target thread: +/// /// // force the operation to execute on a dispatch queue, /// // which may be desirable especially when running /// // the operation without adding it to an OperationQueue /// // and the closure body does not contain any asynchronous code /// let op = AsyncClosureOperation(on: .global()) { -/// // ... do some work ... +/// /// } /// /// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. @@ -65,7 +70,7 @@ public final class AsyncClosureOperation: BasicOperation { override public final func main() { - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } if let queue = queue { queue.async { [weak self] in diff --git a/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift index 83e8046..18b1ef7 100644 --- a/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift +++ b/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift @@ -16,10 +16,13 @@ import Foundation /// /// No special method calls are required in the main block. /// -/// This closure is not cancellable once it is started. If you want to allow cancellation (early return partway through operation execution) use `CancellableClosureOperation` instead. +/// This closure is not cancellable once it is started, and does not offer a reference to update progress information. If you want to allow cancellation (early return partway through operation execution) or progress updating, use `InteractiveClosureOperation` instead. /// /// let op = ClosureOperation { /// // ... do some work ... +/// +/// // operation completes & cleans up automatically +/// // after closure finishes /// } /// /// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. @@ -40,7 +43,9 @@ public final class ClosureOperation: BasicOperation { public final var mainBlock: () -> Void - public init(_ mainBlock: @escaping () -> Void) { + public init( + _ mainBlock: @escaping () -> Void + ) { self.mainBlock = mainBlock @@ -48,7 +53,7 @@ public final class ClosureOperation: BasicOperation { override public func main() { - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } mainBlock() completeOperation() diff --git a/Sources/OTCore/Threading/Operation/Closure/CancellableAsyncClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift similarity index 72% rename from Sources/OTCore/Threading/Operation/Closure/CancellableAsyncClosureOperation.swift rename to Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift index a0d8ff4..cb4144b 100644 --- a/Sources/OTCore/Threading/Operation/Closure/CancellableAsyncClosureOperation.swift +++ b/Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift @@ -1,5 +1,5 @@ // -// CancellableAsyncClosureOperation.swift +// InteractiveAsyncClosureOperation.swift // OTCore • https://github.com/orchetect/OTCore // @@ -14,15 +14,23 @@ import Foundation /// /// **Usage** /// -/// There is no need to guard `mainStartOperation()` at the start of the block, as the initial check is done for you internally. +/// There is no need to guard `mainShouldStart()` at the start of the block, as the initial check is done for you internally. +/// +/// If progress information is available, set `operation.progress.totalUnitCount` and periodically update `operation.progress.completedUnitCount` through the operation. Cleanup will automatically finish the progress and set it to 100% once the block finishes. /// /// It is still best practise to periodically guard `mainShouldAbort()` if the operation may take more than a few seconds. /// /// Finally, you must call `completeOperation()` within the closure block once the async operation is fully finished its execution. /// -/// let op = CancellableAsyncClosureOperation { operation in +/// let op = InteractiveAsyncClosureOperation { operation in +/// // optionally: set progress info +/// operation.progress.totalUnitCount = 100 +/// /// // ... do some work ... /// +/// // optionally: update progress periodically +/// operation.progress.completedUnitCount = 50 +/// /// // optionally: if the operation takes more /// // than a few seconds on average, /// // it's good practise to periodically @@ -31,19 +39,18 @@ import Foundation /// /// // ... do some work ... /// -/// // finally call complete +/// // finally call complete (also sets progress to 100%) /// operation.completeOperation() /// } /// +/// Execution on a target thread: +/// /// // force the operation to execute on a dispatch queue, /// // which may be desirable especially when running /// // the operation without adding it to an OperationQueue /// // and the closure body does not contain any asynchronous code -/// let op = CancellableAsyncClosureOperation(on: .global()) { operation in -/// // ... do some work ... +/// let op = InteractiveAsyncClosureOperation(on: .global()) { operation in /// -/// // finally call complete -/// operation.completeOperation() /// } /// /// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. @@ -58,14 +65,14 @@ import Foundation /// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. /// /// - note: Inherits from both `BasicAsyncOperation` and `BasicOperation`. -public final class CancellableAsyncClosureOperation: BasicAsyncOperation { +public final class InteractiveAsyncClosureOperation: BasicAsyncOperation { public final let queue: DispatchQueue? - public final let mainBlock: (_ operation: CancellableAsyncClosureOperation) -> Void + public final let mainBlock: (_ operation: InteractiveAsyncClosureOperation) -> Void public init( on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: CancellableAsyncClosureOperation) -> Void + _ mainBlock: @escaping (_ operation: InteractiveAsyncClosureOperation) -> Void ) { self.queue = queue @@ -75,7 +82,7 @@ public final class CancellableAsyncClosureOperation: BasicAsyncOperation { override public final func main() { - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } if let queue = queue { queue.async { [weak self] in diff --git a/Sources/OTCore/Threading/Operation/Closure/CancellableClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift similarity index 71% rename from Sources/OTCore/Threading/Operation/Closure/CancellableClosureOperation.swift rename to Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift index 8b7f067..934f7f9 100644 --- a/Sources/OTCore/Threading/Operation/Closure/CancellableClosureOperation.swift +++ b/Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift @@ -1,5 +1,5 @@ // -// CancellableClosureOperation.swift +// InteractiveClosureOperation.swift // OTCore • https://github.com/orchetect/OTCore // @@ -16,9 +16,17 @@ import Foundation /// /// No specific calls are required to be made within the main block, however it is best practise to periodically check if the operation is cancelled and return early if the operation may take more than a few seconds. /// -/// let op = CancellableClosureOperation { operation in +/// If progress information is available, set `operation.progress.totalUnitCount` and periodically update `operation.progress.completedUnitCount` through the operation. Cleanup will automatically finish the progress and set it to 100% once the block finishes. +/// +/// let op = InteractiveClosureOperation { operation in +/// // optionally: set progress info +/// operation.progress.totalUnitCount = 100 +/// /// // ... do some work ... /// +/// // optionally: update progress periodically +/// operation.progress.completedUnitCount = 50 +/// /// // optionally: if the operation takes more /// // than a few seconds on average, /// // it's good practise to periodically @@ -40,13 +48,15 @@ import Foundation /// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. /// /// - note: Inherits from `BasicOperation`. -public final class CancellableClosureOperation: BasicOperation { +public final class InteractiveClosureOperation: BasicOperation { public final override var isAsynchronous: Bool { false } - public final var mainBlock: (_ operation: CancellableClosureOperation) -> Void + public final var mainBlock: (_ operation: InteractiveClosureOperation) -> Void - public init(_ mainBlock: @escaping (_ operation: CancellableClosureOperation) -> Void) { + public init( + _ mainBlock: @escaping (_ operation: InteractiveClosureOperation) -> Void + ) { self.mainBlock = mainBlock @@ -54,7 +64,7 @@ public final class CancellableClosureOperation: BasicOperation { override public func main() { - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } mainBlock(self) completeOperation() diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index 458c7b1..294259e 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -29,7 +29,7 @@ import Foundation /// op.addOperation { atomicValue in /// atomicValue.mutate { $0 += 1 } /// } -/// op.addCancellableOperation { operation, atomicValue in +/// op.addInteractiveOperation { operation, atomicValue in /// atomicValue.mutate { $0 += 1 } /// if operation.mainShouldAbort() { return } /// atomicValue.mutate { $0 += 1 } @@ -58,10 +58,13 @@ open class AtomicBlockOperation: BasicOperation { private let operationQueue: AtomicOperationQueue + /// **OTCore:** + /// Stores a weak reference to the last `Operation` added to the internal operation queue. If the operation is complete and the queue is empty, this may return `nil`. public weak var lastAddedOperation: Operation? { operationQueue.lastAddedOperation } + /// **OTCore:** /// The thread-safe shared mutable value that all operation blocks operate upon. public final var value: T { operationQueue.sharedMutableValue @@ -75,18 +78,33 @@ open class AtomicBlockOperation: BasicOperation { } + /// **OTCore:** + /// Handler called any time the `status` property changes. + public final var statusHandler: BasicOperationQueue.StatusHandler? { + get { + operationQueue.statusHandler + } + set { + operationQueue.statusHandler = newValue + } + } + private var setupBlock: ((_ operation: AtomicBlockOperation, _ atomicValue: AtomicVariableAccess) -> Void)? // MARK: - Init public init(type operationQueueType: OperationQueueType, - initialMutableValue: T) { + initialMutableValue: T, + resetProgressWhenFinished: Bool = false, + statusHandler: BasicOperationQueue.StatusHandler? = nil) { // assign properties - self.operationQueue = AtomicOperationQueue( + operationQueue = AtomicOperationQueue( type: operationQueueType, - initialMutableValue: initialMutableValue + initialMutableValue: initialMutableValue, + resetProgressWhenFinished: resetProgressWhenFinished, + statusHandler: statusHandler ) // super @@ -107,7 +125,7 @@ open class AtomicBlockOperation: BasicOperation { public final override func main() { - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } let varAccess = AtomicVariableAccess(operationQueue: self.operationQueue) setupBlock?(self, varAccess) @@ -122,7 +140,10 @@ open class AtomicBlockOperation: BasicOperation { // which mirrors the behavior of BlockOperation while !isFinished { usleep(10_000) // 10 milliseconds - //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) // DO NOT DO THIS!!! + + //Thread.sleep(forTimeInterval: 0.010) // 10 milliseconds + + //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) // 10 milliseconds } } @@ -134,36 +155,63 @@ open class AtomicBlockOperation: BasicOperation { private var observers: [NSKeyValueObservation] = [] private func addObservers() { - let isCancelledRetain = observe(\.isCancelled, - options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } - if self.isCancelled { - self.operationQueue.cancelAllOperations() - self.completeOperation() + // self.isCancelled + + observers.append( + observe(\.isCancelled, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + if newValue { + self.operationQueue.cancelAllOperations() + self.completeOperation(dueToCancellation: true) + } } - } - observers.append(isCancelledRetain) + ) - let qosRetain = observe(\.qualityOfService, - options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } - // propagate to operation queue - self.operationQueue.qualityOfService = self.qualityOfService - } - observers.append(qosRetain) + // self.operationQueue.operationCount + + observers.append( + operationQueue.observe(\.operationCount, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + if newValue == 0 { + self.completeOperation() + } + } + ) - // can't use operationQueue.progress as it's macOS 10.15+ only - let opCtRetain = operationQueue.observe(\.operationCount, - options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } - if self.operationQueue.operationCount == 0 { - self.completeOperation() + // self.qualityOfService + + observers.append( + observe(\.qualityOfService, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + + // for some reason, change.newValue is nil here. so just read from the property directly. + // guard let newValue = change.newValue else { return } + + // propagate to operation queue + self.operationQueue.qualityOfService = self.qualityOfService } - } - observers.append(opCtRetain) + ) + + // self.operationQueue.progress.isFinished + + observers.append( + operationQueue.progress.observe(\.isFinished, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + if newValue { + self.completeOperation() + } + } + ) } @@ -200,13 +248,13 @@ extension AtomicBlockOperation { /// /// - returns: The new operation. @discardableResult - public final func addCancellableOperation( + public final func addInteractiveOperation( dependencies: [Operation] = [], - _ block: @escaping (_ operation: CancellableClosureOperation, + _ block: @escaping (_ operation: InteractiveClosureOperation, _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { + ) -> InteractiveClosureOperation { - operationQueue.addCancellableOperation(dependencies: dependencies, block) + operationQueue.addInteractiveOperation(dependencies: dependencies, block) } @@ -241,6 +289,7 @@ extension AtomicBlockOperation { } + /// **OTCore:** /// Blocks the current thread until all the receiver’s queued and executing operations finish executing. public func waitUntilAllOperationsAreFinished(timeout: DispatchTimeInterval? = nil) { diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift index 87cf9cd..7f5f4ab 100644 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift +++ b/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift @@ -11,7 +11,13 @@ import Foundation /// An asynchronous `Operation` subclass that provides essential boilerplate. /// `BasicAsyncOperation` is designed to be subclassed. /// -/// This operation is asynchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread and may return control before the operation is complete. +/// **Important Information from Apple Docs** +/// +/// If you always plan to use queues to execute your operations, it is simpler to define them as synchronous (by subclassing OTCore's `BasicOperation` instead). If you execute operations manually, though, you might want to define your operation objects as asynchronous. Defining an asynchronous operation requires more work, because you have to monitor the ongoing state of your task and report changes in that state using KVO notifications. But defining asynchronous operations is useful in cases where you want to ensure that a manually executed operation does not block the calling thread. +/// +/// When you call the `start()` method of an asynchronous operation, that method may return before the corresponding task is completed. An asynchronous operation object is responsible for scheduling its task on a separate thread. The operation could do that by starting a new thread directly, by calling an asynchronous method, or by submitting a block to a dispatch queue for execution. It does not actually matter if the operation is ongoing when control returns to the caller, only that it could be ongoing. +/// +/// When you add an operation to an operation queue, the queue ignores the value of the `isAsynchronous` property and always calls the `start()` method from a separate thread. Therefore, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous. /// /// **Usage** /// @@ -22,7 +28,7 @@ import Foundation /// class MyOperation: BasicAsyncOperation { /// override func main() { /// // At the start, call this and conditionally return: -/// guard mainStartOperation() else { return } +/// guard mainShouldStart() else { return } /// /// // ... do some work ... /// diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift index 5515976..4a4c125 100644 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift +++ b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift @@ -24,7 +24,7 @@ import Foundation /// class MyOperation: BasicOperation { /// override func main() { /// // At the start, call this and conditionally return: -/// guard mainStartOperation() else { return } +/// guard mainShouldStart() else { return } /// /// // ... do some work ... /// @@ -41,7 +41,13 @@ import Foundation /// } /// /// - important: This object is designed to be subclassed. See the Foundation documentation for `Operation` regarding overriding `start()` and be sure to follow the guidelines in these inline docs regarding `BasicOperation` specifically. -open class BasicOperation: Operation { +open class BasicOperation: Operation, ProgressReporting { + + // MARK: - Progress + + /// **OTCore:** + /// Progress object representing progress of the operation. + @Atomic public var progress: Progress = .init(totalUnitCount: 1) // MARK: - KVO @@ -62,6 +68,10 @@ open class BasicOperation: Operation { // adding KVO compliance @objc dynamic public final override var qualityOfService: QualityOfService { + get { _qualityOfService } + set { _qualityOfService = newValue } + } + private var _qualityOfService: QualityOfService = .default { willSet { willChangeValue(for: \.qualityOfService) } didSet { didChangeValue(for: \.qualityOfService) } } @@ -69,17 +79,23 @@ open class BasicOperation: Operation { // MARK: - Method Overrides public final override func start() { - if isCancelled { completeOperation() } + if isCancelled { completeOperation(dueToCancellation: true) } super.start() } + public final override func cancel() { + super.cancel() + progress.cancel() + } + // MARK: - Methods + /// **OTCore:** /// Returns true if operation should begin. - public final func mainStartOperation() -> Bool { + public final func mainShouldStart() -> Bool { guard !isCancelled else { - completeOperation() + completeOperation(dueToCancellation: true) return false } @@ -89,20 +105,29 @@ open class BasicOperation: Operation { } + /// **OTCore:** /// Call this once all execution is complete in the operation. - public final func completeOperation() { + /// If returning early from the operation due to `isCancelled` being true, call this with the `dueToCancellation` flag set to `true` to update this operation's progress as cancelled. + public final func completeOperation(dueToCancellation: Bool = false) { + + if isCancelled || dueToCancellation { + progress.cancel() + } else { + progress.completedUnitCount = progress.totalUnitCount + } _isExecuting = false _isFinished = true } + /// **OTCore:** /// Checks if `isCancelled` is true, and calls `completedOperation()` if so. /// Returns `isCancelled`. public final func mainShouldAbort() -> Bool { if isCancelled { - completeOperation() + completeOperation(dueToCancellation: true) } return isCancelled diff --git a/Sources/OTCore/Threading/Operation/Operation Extensions.swift b/Sources/OTCore/Threading/Operation/Operation Extensions.swift index 37ae881..ed96b73 100644 --- a/Sources/OTCore/Threading/Operation/Operation Extensions.swift +++ b/Sources/OTCore/Threading/Operation/Operation Extensions.swift @@ -20,23 +20,24 @@ extension Operation { } /// **OTCore:** - /// Convenience static constructor for `CancellableClosureOperation`. - public static func cancellable( - _ mainBlock: @escaping (_ operation: CancellableClosureOperation) -> Void - ) -> CancellableClosureOperation { + /// Convenience static constructor for `InteractiveClosureOperation`. + public static func interactive( + _ mainBlock: @escaping (_ operation: InteractiveClosureOperation) -> Void + ) -> InteractiveClosureOperation { .init(mainBlock) } /// **OTCore:** - /// Convenience static constructor for `CancellableAsyncClosureOperation`. - public static func cancellableAsync( + /// Convenience static constructor for `InteractiveAsyncClosureOperation`. + public static func interactiveAsync( on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: CancellableAsyncClosureOperation) -> Void - ) -> CancellableAsyncClosureOperation { + _ mainBlock: @escaping (_ operation: InteractiveAsyncClosureOperation) -> Void + ) -> InteractiveAsyncClosureOperation { - .init(on: queue, mainBlock) + .init(on: queue, + mainBlock) } diff --git a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift index f9db94e..15eea07 100644 --- a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift @@ -21,12 +21,16 @@ open class AtomicOperationQueue: BasicOperationQueue { public init( type operationQueueType: OperationQueueType = .concurrentAutomatic, - initialMutableValue: T + initialMutableValue: T, + resetProgressWhenFinished: Bool = false, + statusHandler: BasicOperationQueue.StatusHandler? = nil ) { self.sharedMutableValue = initialMutableValue - super.init(type: operationQueueType) + super.init(type: operationQueueType, + resetProgressWhenFinished: resetProgressWhenFinished, + statusHandler: statusHandler) } @@ -54,13 +58,13 @@ open class AtomicOperationQueue: BasicOperationQueue { /// /// - returns: The new operation. @discardableResult - public final func addCancellableOperation( + public final func addInteractiveOperation( dependencies: [Operation] = [], - _ block: @escaping (_ operation: CancellableClosureOperation, + _ block: @escaping (_ operation: InteractiveClosureOperation, _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { + ) -> InteractiveClosureOperation { - let op = createCancellableOperation(block) + let op = createInteractiveOperation(block) dependencies.forEach { op.addDependency($0) } addOperation(op) return op @@ -105,12 +109,12 @@ open class AtomicOperationQueue: BasicOperationQueue { /// Internal for debugging: /// Create an operation block operating on the shared mutable value. /// `operation.mainShouldAbort()` can be periodically called and then early return if the operation may take more than a few seconds. - internal final func createCancellableOperation( - _ block: @escaping (_ operation: CancellableClosureOperation, + internal final func createInteractiveOperation( + _ block: @escaping (_ operation: InteractiveClosureOperation, _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { + ) -> InteractiveClosureOperation { - CancellableClosureOperation { [weak self] operation in + InteractiveClosureOperation { [weak self] operation in guard let self = self else { return } let varAccess = AtomicVariableAccess(operationQueue: self) block(operation, varAccess) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift new file mode 100644 index 0000000..8e84287 --- /dev/null +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift @@ -0,0 +1,30 @@ +// +// BasicOperationQueue Status.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if canImport(Foundation) + +import Foundation + +/// **OTCore:** +/// Operation queue status. +/// Used by `BasicOperationQueue` and its subclasses. +public enum OperationQueueStatus: Equatable, Hashable { + + /// Operation queue is empty. No operations are executing. + case idle + + /// Operation queue is executing one or more operations. + /// - Parameters: + /// - fractionCompleted: progress between 0.0...1.0 + /// - message: displayable string describing the current operation + case inProgress(fractionCompleted: Double, message: String) + + /// Operation queue is paused. + /// There may or may not be operations in the queue. + case paused + +} + +#endif diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 43e2934..259452f 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -11,6 +11,10 @@ import Foundation /// An `OperationQueue` subclass with useful additions. open class BasicOperationQueue: OperationQueue { + /// **OTCore:** + /// Any time the queue completes all of its operations and returns to an empty queue, reset the progress object's total unit count to 0. + public final var resetProgressWhenFinished: Bool + /// **OTCore:** /// A reference to the `Operation` that was last added to the queue. Returns `nil` if the operation finished and no longer exists. public final weak var lastAddedOperation: Operation? @@ -38,13 +42,41 @@ open class BasicOperationQueue: OperationQueue { } + // MARK: - Status + + public internal(set) var status: OperationQueueStatus = .idle { + didSet { + if status != oldValue { + statusHandler?(status, oldValue) + } + } + } + + public typealias StatusHandler = (_ newStatus: OperationQueueStatus, + _ oldStatus: OperationQueueStatus) -> Void + + /// **OTCore:** + /// Handler called any time the `status` property changes. + public final var statusHandler: StatusHandler? + + // MARK: - Progress Back-Porting + + @Atomic private var _progress: Progress = .init() + + @available(macOS 10.9, iOS 7.0, tvOS 9.0, watchOS 2.0, *) + public override final var progress: Progress { _progress } + // MARK: - Init /// **OTCore:** /// Set max concurrent operation count. - public init(type operationQueueType: OperationQueueType) { + public init(type operationQueueType: OperationQueueType, + resetProgressWhenFinished: Bool = false, + statusHandler: StatusHandler? = nil) { self.operationQueueType = operationQueueType + self.resetProgressWhenFinished = resetProgressWhenFinished + self.statusHandler = statusHandler super.init() @@ -70,6 +102,14 @@ open class BasicOperationQueue: OperationQueue { break } + // update progress + progress.totalUnitCount += 1 + if let basicOp = op as? BasicOperation { + // OperationQueue considers each operation to be 1 unit of progress in the overall queue progress, regardless of how the child operation progress decides to set up its total unit count + progress.addChild(basicOp.progress, + withPendingUnitCount: 1) + } + lastAddedOperation = op super.addOperation(op) @@ -113,12 +153,89 @@ open class BasicOperationQueue: OperationQueue { break } - lastAddedOperation = ops.last + // update progress + progress.totalUnitCount += Int64(ops.count) + for op in ops { + if let basicOp = op as? BasicOperation { + // OperationQueue considers each operation to be 1 unit of progress in the overall queue progress, regardless of how the child operation progress decides to set up its total unit count + progress.addChild(basicOp.progress, + withPendingUnitCount: 1) + } + } + lastAddedOperation = ops.last super.addOperations(ops, waitUntilFinished: wait) } + // MARK: - KVO Observers + + /// **OTCore:** + /// Retain property observers. For safety, this array must be emptied on class deinit. + private var observers: [NSKeyValueObservation] = [] + private func addObservers() { + + // self.progress.isFinished + + observers.append( + progress.observe(\.isFinished, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + if newValue { + if self.resetProgressWhenFinished { + self.progress.totalUnitCount = 0 + } + self.status = .idle + } + } + ) + + // self.progress.fractionCompleted + + observers.append( + progress.observe(\.fractionCompleted, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + guard !self.progress.isFinished else { return } + self.status = .inProgress(fractionCompleted: newValue, + message: self.progress.localizedDescription) + } + ) + + // self.isSuspended + + observers.append( + observe(\.isSuspended, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + guard let newValue = change.newValue else { return } + + if newValue { + self.status = .paused + } else { + if self.operationCount > 0 { + self.status = .inProgress( + fractionCompleted: self.progress.fractionCompleted, + message: self.progress.localizedDescription + ) + } else { + self.status = .idle + } + } + } + ) + + } + + deinit { + // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time + observers.removeAll() + } + } #endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift index 5221e17..5c1d773 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift @@ -32,9 +32,15 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -57,10 +63,16 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -95,16 +107,23 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { usleep(200_000) // give a little time for cleanup + // state + XCTAssertFalse(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertTrue(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertLessThan(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") @@ -118,29 +137,40 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. func testQueue_Cancel() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") // the operation's main block does finish eventually but won't finish in time for our timeout because there's no opportunity to return early from cancelling the operation let mainBlockFinishedExp = expectation(description: "Main Block Finished") - mainBlockFinishedExp.isInverted = true // the operation's completion block does not fire in time because there's no opportunity to return early from cancelling the operation let completionBlockExp = expectation(description: "Completion Block Called") @@ -148,7 +178,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { let op = AsyncClosureOperation(on: .global()) { mainBlockExp.fulfill() - sleep(4) // seconds + sleep(1) // seconds mainBlockFinishedExp.fulfill() } @@ -156,23 +186,56 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 + + XCTAssertEqual(op.progress.totalUnitCount, 1) + XCTAssertEqual(opQ.progress.totalUnitCount, 1) + // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) usleep(100_000) // 100 milliseconds - oq.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. + opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - - usleep(200_000) // give a little time for cleanup + wait(for: [mainBlockExp, completionBlockExp], timeout: 0.4) + // state // the operation is still running because it cannot return early from being cancelled - XCTAssertEqual(oq.operationCount, 1) - + XCTAssertEqual(opQ.operationCount, 1) + XCTAssertFalse(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertTrue(op.isExecuting) // still executing - XCTAssertFalse(op.isFinished) // not yet finished - + // progress - operation + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) // even if the async op is still running, this will be true now + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) + + wait(for: [mainBlockFinishedExp], timeout: 0.7) + usleep(100_000) // 100 milliseconds + + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) + XCTAssertTrue(op.isCancelled) + XCTAssertFalse(op.isExecuting) + // progress - operation + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) + } } diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift index 8f94f13..bd14b15 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift @@ -40,9 +40,14 @@ final class Threading_ClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -70,17 +75,61 @@ final class Threading_ClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) + + } + + /// Test as a standalone operation. Cancel it before it runs. + func testOpCancelBeforeRun() { + + let mainBlockExp = expectation(description: "Main Block Called") + mainBlockExp.isInverted = true + + let op = ClosureOperation { + self.mainCheck() + } + + // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure + mainCheck = { + mainBlockExp.fulfill() + XCTAssertTrue(op.isExecuting) + } + + let completionBlockExp = expectation(description: "Completion Block Called") + + op.completionBlock = { + completionBlockExp.fulfill() + } + + op.cancel() + op.start() // in an OperationQueue, all operations must start even if they're already cancelled + + wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) + + // state + XCTAssertTrue(op.isReady) + XCTAssertTrue(op.isFinished) + XCTAssertTrue(op.isCancelled) + XCTAssertFalse(op.isExecuting) + // progress + XCTAssertTrue(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") @@ -100,16 +149,27 @@ final class Threading_ClosureOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } @@ -140,11 +200,15 @@ final class Threading_ClosureOperation_Tests: XCTestCase { op.start() + // state XCTAssertEqual(val, 1) - + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) wait(for: [mainBlockExp, completionBlockExp], timeout: 2) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift similarity index 59% rename from Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift rename to Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift index b2f3244..06a7462 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift @@ -1,5 +1,5 @@ // -// CancellableAsyncClosureOperation Tests.swift +// InteractiveAsyncClosureOperation Tests.swift // OTCore • https://github.com/orchetect/OTCore // @@ -8,7 +8,7 @@ import OTCore import XCTest -final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { +final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { func testOpRun() { @@ -16,7 +16,7 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { let completionBlockExp = expectation(description: "Completion Block Called") - let op = CancellableAsyncClosureOperation { operation in + let op = InteractiveAsyncClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) operation.completeOperation() @@ -30,9 +30,15 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -44,7 +50,7 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { let completionBlockExp = expectation(description: "Completion Block Called") completionBlockExp.isInverted = true - let op = CancellableAsyncClosureOperation { operation in + let op = InteractiveAsyncClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) operation.completeOperation() @@ -56,10 +62,16 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -70,12 +82,17 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { let completionBlockExp = expectation(description: "Completion Block Called") - let op = CancellableAsyncClosureOperation(on: .global()) { operation in + let op = InteractiveAsyncClosureOperation(on: .global()) { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) - for _ in 1...100 { // finishes in 20 seconds + operation.progress.totalUnitCount = 100 + + for i in 1...100 { // finishes in 20 seconds + operation.progress.completedUnitCount = Int64(i) + usleep(200_000) // 200 milliseconds + // would call this once ore more throughout the operation if operation.mainShouldAbort() { return } } @@ -87,81 +104,102 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { completionBlockExp.fulfill() } - print("start() in:", Date()) op.start() - print("start() out:", Date()) - usleep(100_000) // 100 milliseconds - - print("cancel() in:", Date()) op.cancel() // cancel the operation directly (since we are not using an OperationQueue) - print("cancel() out:", Date()) wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) + XCTAssertLessThan(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") + let mainBlockFinishedExp = expectation(description: "Main Block Finished") + let completionBlockExp = expectation(description: "Completion Block Called") - let op = CancellableAsyncClosureOperation { operation in + let op = InteractiveAsyncClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) operation.completeOperation() + mainBlockFinishedExp.fulfill() } op.completionBlock = { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) + wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. func testQueue_Cancel() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") // main block does not finish because we return early from cancelling the operation let mainBlockFinishedExp = expectation(description: "Main Block Finished") - mainBlockFinishedExp.isInverted = true // completion block still successfully fires because our early return from being cancelled marks the operation as isFinished == true let completionBlockExp = expectation(description: "Completion Block Called") - let op = CancellableAsyncClosureOperation { operation in + let op = InteractiveAsyncClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) - for _ in 1...100 { // finishes in 20 seconds + defer { mainBlockFinishedExp.fulfill() } + + operation.progress.totalUnitCount = 100 + + for i in 1...100 { // finishes in 20 seconds + operation.progress.completedUnitCount = Int64(i) + usleep(200_000) // 200 milliseconds + // would call this once ore more throughout the operation if operation.mainShouldAbort() { return } } - mainBlockFinishedExp.fulfill() operation.completeOperation() } @@ -169,19 +207,32 @@ final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) usleep(100_000) // 100 milliseconds - oq.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. + opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) + XCTAssertLessThan(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift similarity index 88% rename from Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift rename to Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift index 2076145..4d54f6e 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift @@ -1,5 +1,5 @@ // -// CancellableClosureOperation Tests.swift +// InteractiveClosureOperation Tests.swift // OTCore • https://github.com/orchetect/OTCore // @@ -8,13 +8,13 @@ import OTCore import XCTest -final class Threading_CancellableClosureOperation_Tests: XCTestCase { +final class Threading_InteractiveClosureOperation_Tests: XCTestCase { func testOpRun() { let mainBlockExp = expectation(description: "Main Block Called") - let op = CancellableClosureOperation { operation in + let op = InteractiveClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) @@ -44,7 +44,7 @@ final class Threading_CancellableClosureOperation_Tests: XCTestCase { let mainBlockExp = expectation(description: "Main Block Called") mainBlockExp.isInverted = true - let op = CancellableClosureOperation { operation in + let op = InteractiveClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) @@ -72,11 +72,11 @@ final class Threading_CancellableClosureOperation_Tests: XCTestCase { /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let mainBlockExp = expectation(description: "Main Block Called") - let op = CancellableClosureOperation { operation in + let op = InteractiveClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) @@ -92,11 +92,11 @@ final class Threading_CancellableClosureOperation_Tests: XCTestCase { } // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) + XCTAssertEqual(opQ.operationCount, 0) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) @@ -113,7 +113,7 @@ final class Threading_CancellableClosureOperation_Tests: XCTestCase { var val = 0 - let op = CancellableClosureOperation { operation in + let op = InteractiveClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) usleep(500_000) // 500 milliseconds diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift index 4cb40cb..600288e 100644 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift @@ -17,9 +17,9 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { op.start() + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -51,9 +51,9 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -92,9 +92,9 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { XCTAssertEqual(op.value.count, 100) XCTAssert(Array(1...100).allSatisfy(op.value.contains)) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -105,8 +105,10 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { initialMutableValue: [Int]()) let completionBlockExp = expectation(description: "Completion Block Called") + completionBlockExp.isInverted = true let dataVerificationExp = expectation(description: "Data Verification") + dataVerificationExp.isInverted = true for val in 1...100 { op.addOperation { $0.mutate { $0.append(val) } } @@ -126,13 +128,12 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { } } - op.start() - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) + // state + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -146,6 +147,8 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { let dataVerificationExp = expectation(description: "Data Verification") + let atomicBlockCompletedExp = expectation(description: "AtomicBlockOperation Completed") + for val in 1...100 { op.addOperation { $0.mutate { $0.append(val) } } } @@ -164,23 +167,26 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { } } - op.start() + DispatchQueue.global().async { + op.start() + atomicBlockCompletedExp.fulfill() + } - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) + wait(for: [completionBlockExp, dataVerificationExp, atomicBlockCompletedExp], timeout: 1) XCTAssertEqual(op.value.count, 100) XCTAssert(Array(1...100).allSatisfy(op.value.contains)) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } /// Test in the context of an OperationQueue. Run is implicit. func testOp_concurrentAutomatic_Queue() { - let oq = OperationQueue() + let opQ = OperationQueue() let op = AtomicBlockOperation(type: .concurrentAutomatic, initialMutableValue: [Int]()) @@ -188,7 +194,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { // test default qualityOfService to check baseline state XCTAssertEqual(op.qualityOfService, .default) - op.qualityOfService = .utility + op.qualityOfService = .userInitiated let completionBlockExp = expectation(description: "Completion Block Called") @@ -197,7 +203,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { for val in 1...100 { op.addOperation { v in // QoS should be inherited from the AtomicBlockOperation QoS - XCTAssertEqual(Thread.current.qualityOfService, .utility) + XCTAssertEqual(Thread.current.qualityOfService, .userInitiated) // add value to array v.mutate { $0.append(val) } @@ -218,16 +224,18 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { } } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -255,9 +263,10 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { // check that all operations executed and they are in serial FIFO order XCTAssertEqual(op.value, Array(1...100)) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) wait(for: [completionBlockExp], timeout: 2) @@ -303,11 +312,12 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { mainOp.start() - wait(for: [completionBlockExp], timeout: 10) + wait(for: [completionBlockExp], timeout: 5) + // state + XCTAssertTrue(mainOp.isFinished) XCTAssertFalse(mainOp.isCancelled) XCTAssertFalse(mainOp.isExecuting) - XCTAssertTrue(mainOp.isFinished) XCTAssertEqual(mainVal.count, 10) XCTAssertEqual(mainVal.keys.sorted(), Array(1...10)) @@ -329,7 +339,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { initialMutableValue: [Int]()) var refs: [Operation] = [] for valueNum in 1...20 { - let ref = subOp.addCancellableOperation { op, v in + let ref = subOp.addInteractiveOperation { op, v in if op.mainShouldAbort() { return } usleep(200_000) v.mutate { value in @@ -360,23 +370,28 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must run start() async in order to cancel it, since + // the operation is synchronous and will complete before we + // can call cancel() if start() is run in-thread DispatchQueue.global().async { mainOp.start() } usleep(100_000) // 100 milliseconds mainOp.cancel() - wait(for: [completionBlockExp], timeout: 5) + wait(for: [completionBlockExp], timeout: 1) //XCTAssertEqual(mainOp.operationQueue.operationCount, 0) - XCTAssertTrue(mainOp.isCancelled) - XCTAssertFalse(mainOp.isExecuting) + // state XCTAssertTrue(mainOp.isFinished) + XCTAssertTrue(mainOp.isCancelled) + XCTAssertFalse(mainOp.isExecuting) // TODO: technically this should be true, but it gets set to false when the completion method gets called even if async code is still running - XCTAssert(!mainVal.values.allSatisfy({ $0.sorted() == Array(1...200)})) - - dump(mainOp) + let expectedArray = (1...10).reduce(into: [Int: [Int]]()) { + $0[$1] = Array(1...200) + } + XCTAssertNotEqual(mainVal, expectedArray) } diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift index a048871..fe888ca 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift @@ -22,7 +22,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { override func main() { print("Starting main()") - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } XCTAssertTrue(isExecuting) @@ -44,21 +44,33 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { /// This is a simple subclass to test. private class TestLongRunningBasicAsyncOperation: BasicAsyncOperation { + private let totalOpCount = 100 + + override init() { + super.init() + progress.totalUnitCount = Int64(totalOpCount) + } + override func main() { print("Starting main()") - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } XCTAssertTrue(isExecuting) // run a simple non-blocking loop that can frequently check for cancellation - DispatchQueue.global().async { - for _ in 1...100 { // finishes in 20 seconds - usleep(200_000) // 200 milliseconds + DispatchQueue.global().async { [weak self] in + guard let self = self else { return } + + for opNum in 1...self.totalOpCount { // finishes in 20 seconds // it's good to call this once or more throughout the operation // so we can return early if the operation is cancelled if self.mainShouldAbort() { return } + + self.progress.completedUnitCount = Int64(opNum) + + usleep(200_000) // 200 milliseconds } self.completeOperation() @@ -85,9 +97,15 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -105,10 +123,16 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -129,16 +153,23 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) + XCTAssertLessThan(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let op = TestBasicAsyncOperation() @@ -148,23 +179,35 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. func testQueue_Cancel() { - let oq = OperationQueue() + let opQ = OperationQueue() let op = TestLongRunningBasicAsyncOperation() @@ -174,19 +217,32 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) usleep(100_000) // 100 milliseconds - oq.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. + opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. wait(for: [completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) + XCTAssertLessThan(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift index b4deaed..ee76e22 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift @@ -22,7 +22,7 @@ final class Threading_BasicOperation_Tests: XCTestCase { override func main() { print("Starting main()") - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } XCTAssertTrue(isExecuting) @@ -43,15 +43,19 @@ final class Threading_BasicOperation_Tests: XCTestCase { public var val: Int private var valChangeTo: Int - public init(initial: Int, changeTo: Int) { + public init(initial: Int, + changeTo: Int) { + val = initial valChangeTo = changeTo + super.init() + } override func main() { print("Starting main()") - guard mainStartOperation() else { return } + guard mainShouldStart() else { return } XCTAssertTrue(isExecuting) @@ -85,9 +89,15 @@ final class Threading_BasicOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.5) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) } @@ -105,15 +115,21 @@ final class Threading_BasicOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } - /// Test as a standalone operation. Do not run it. Cancel it before it runs. - func testOpNotRun_Cancel() { + /// Test as a standalone operation. Cancel it before it runs. + func testOpCancelBeforeRun() { let op = TestBasicOperation() @@ -128,17 +144,23 @@ final class Threading_BasicOperation_Tests: XCTestCase { wait(for: [completionBlockExp], timeout: 0.3) + // state XCTAssertTrue(op.isReady) + XCTAssertTrue(op.isFinished) XCTAssertTrue(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertFalse(op.progress.isFinished) + XCTAssertTrue(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 0.0) + XCTAssertFalse(op.progress.isIndeterminate) } /// Test in the context of an OperationQueue. Run is implicit. func testQueue() { - let oq = OperationQueue() + let opQ = OperationQueue() let op = TestBasicOperation() @@ -148,16 +170,28 @@ final class Threading_BasicOperation_Tests: XCTestCase { completionBlockExp.fulfill() } + // must manually increment for OperationQueue + opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added - oq.addOperation(op) + opQ.addOperation(op) wait(for: [completionBlockExp], timeout: 0.5) - XCTAssertEqual(oq.operationCount, 0) - + // state + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress - operation + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) + // progress - queue + XCTAssertTrue(opQ.progress.isFinished) + XCTAssertFalse(opQ.progress.isCancelled) + XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) + XCTAssertFalse(opQ.progress.isIndeterminate) } @@ -167,7 +201,8 @@ final class Threading_BasicOperation_Tests: XCTestCase { let completionBlockExp = expectation(description: "Completion Block Called") // after start(), will mutate self after 500ms then finish - let op = TestDelayedMutatingBasicOperation(initial: 0, changeTo: 1) + let op = TestDelayedMutatingBasicOperation(initial: 0, + changeTo: 1) op.completionBlock = { completionBlockExp.fulfill() @@ -177,9 +212,15 @@ final class Threading_BasicOperation_Tests: XCTestCase { XCTAssertEqual(op.val, 1) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) + // progress + XCTAssertTrue(op.progress.isFinished) + XCTAssertFalse(op.progress.isCancelled) + XCTAssertEqual(op.progress.fractionCompleted, 1.0) + XCTAssertFalse(op.progress.isIndeterminate) wait(for: [completionBlockExp], timeout: 2) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index 09b374b..72b4900 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -110,7 +110,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { ops.append(op) } for val in 51...100 { - let op = oq.createCancellableOperation { _, v in + let op = oq.createInteractiveOperation { _, v in v.mutate { $0.append(val) } } ops.append(op) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift index 515b0d9..6046dcf 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift @@ -15,44 +15,44 @@ final class Threading_OperationQueueExtensions_Tests: XCTestCase { func testWaitUntilAllOperationsAreFinished_Timeout_Success() { - let oq = OperationQueue() - oq.maxConcurrentOperationCount = 1 // serial - oq.isSuspended = true + let opQ = OperationQueue() + opQ.maxConcurrentOperationCount = 1 // serial + opQ.isSuspended = true var val = 0 - oq.addOperation { + opQ.addOperation { usleep(100_000) // 100 milliseconds val = 1 } - oq.isSuspended = false - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(5)) + opQ.isSuspended = false + let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(5)) XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.operationCount, 0) + XCTAssertEqual(opQ.operationCount, 0) XCTAssertEqual(val, 1) } func testWaitUntilAllOperationsAreFinished_Timeout_TimedOut() { - let oq = OperationQueue() - oq.maxConcurrentOperationCount = 1 // serial - oq.isSuspended = true + let opQ = OperationQueue() + opQ.maxConcurrentOperationCount = 1 // serial + opQ.isSuspended = true var val = 0 - oq.addOperation { + opQ.addOperation { sleep(5) // 5 seconds val = 1 } - oq.isSuspended = false - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) + opQ.isSuspended = false + let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) XCTAssertEqual(timeoutResult, .timedOut) - XCTAssertEqual(oq.operationCount, 1) + XCTAssertEqual(opQ.operationCount, 1) XCTAssertEqual(val, 0) } From e6fd6000267a0bfae02adca25a9e009b726cd74b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:19:10 -0800 Subject: [PATCH 02/31] Added `DispatchTimeInterval.microseconds` and `sleep(DispatchTimeInterval)` --- .../Dispatch/DispatchTimeInterval.swift | 54 +++++++++++++++++++ .../Dispatch/DispatchTimeInterval Tests.swift | 48 +++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 Sources/OTCore/Extensions/Dispatch/DispatchTimeInterval.swift create mode 100644 Tests/OTCoreTests/Extensions/Dispatch/DispatchTimeInterval Tests.swift diff --git a/Sources/OTCore/Extensions/Dispatch/DispatchTimeInterval.swift b/Sources/OTCore/Extensions/Dispatch/DispatchTimeInterval.swift new file mode 100644 index 0000000..0e159db --- /dev/null +++ b/Sources/OTCore/Extensions/Dispatch/DispatchTimeInterval.swift @@ -0,0 +1,54 @@ +// +// DispatchTimeInterval.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if canImport(Dispatch) + +import Dispatch + +extension DispatchTimeInterval { + + /// **OTCore:** + /// Return the interval as `Int` seconds. + public var microseconds: Int { + + switch self { + case .seconds(let val): + return val * 1_000_000 + + case .milliseconds(let val): // ms + return val * 1_000 + + case .microseconds(let val): // µs + return val + + case .nanoseconds(let val): // ns + return val / 1_000 + + case .never: + assertionFailure("Cannot convert 'never' to microseconds.") + return 0 + + @unknown default: + assertionFailure("Unhandled DispatchTimeInterval case when attempting to convert to microseconds.") + return 0 + + } + + } + +} + +/// **OTCore:** +/// Convenience to convert a `DispatchTimeInterval` to microseconds and run `usleep()`. +@_disfavoredOverload +public func sleep(_ dispatchTimeInterval: DispatchTimeInterval) { + + let ms = dispatchTimeInterval.microseconds + guard ms > 0 else { return } + usleep(UInt32(ms)) + +} + +#endif diff --git a/Tests/OTCoreTests/Extensions/Dispatch/DispatchTimeInterval Tests.swift b/Tests/OTCoreTests/Extensions/Dispatch/DispatchTimeInterval Tests.swift new file mode 100644 index 0000000..eab69f7 --- /dev/null +++ b/Tests/OTCoreTests/Extensions/Dispatch/DispatchTimeInterval Tests.swift @@ -0,0 +1,48 @@ +// +// DispatchTimeInterval Tests.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if shouldTestCurrentPlatform + +import XCTest +@testable import OTCore + +class Extensions_Dispatch_DispatchTimeInterval_Tests: XCTestCase { + + override func setUp() { super.setUp() } + override func tearDown() { super.tearDown() } + + func testMicroseconds() { + + XCTAssertEqual( + DispatchTimeInterval.seconds(2).microseconds, + 2_000_000 + ) + + XCTAssertEqual( + DispatchTimeInterval.milliseconds(2_000).microseconds, + 2_000_000 + ) + + XCTAssertEqual( + DispatchTimeInterval.microseconds(2_000_000).microseconds, + 2_000_000 + ) + + XCTAssertEqual( + DispatchTimeInterval.nanoseconds(2_000_000_000).microseconds, + 2_000_000 + ) + + // assertion error: + //XCTAssertEqual( + // DispatchTimeInterval.never.microseconds, + // 0 + //) + + } + +} + +#endif From c4615816be17270ce4932a2963005861d699e752 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:19:42 -0800 Subject: [PATCH 03/31] Added `sleep(TimeInterval)` --- .../Foundation/Darwin and Foundation.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 Sources/OTCore/Extensions/Foundation/Darwin and Foundation.swift diff --git a/Sources/OTCore/Extensions/Foundation/Darwin and Foundation.swift b/Sources/OTCore/Extensions/Foundation/Darwin and Foundation.swift new file mode 100644 index 0000000..de74d2d --- /dev/null +++ b/Sources/OTCore/Extensions/Foundation/Darwin and Foundation.swift @@ -0,0 +1,21 @@ +// +// Darwin and Foundation.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if canImport(Foundation) + +import Foundation + +/// **OTCore:** +/// Convenience to convert a `TimeInterval` to microseconds and run `usleep()`. +@_disfavoredOverload +public func sleep(_ timeInterval: TimeInterval) { + + let ms = timeInterval * 1_000_000 + guard ms > 0.0 else { return } + usleep(UInt32(ms)) + +} + +#endif From aabd17838da0ad4ffb3b193c1716f6ad4f201fcb Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:20:35 -0800 Subject: [PATCH 04/31] `DispatchTimeInterval`: Added `timeInterval` property --- .../Foundation/Dispatch and Foundation.swift | 58 ++++++++++++++++++- .../Dispatch and Foundation Tests.swift | 36 ++++++++++++ 2 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 Tests/OTCoreTests/Extensions/Foundation/Dispatch and Foundation Tests.swift diff --git a/Sources/OTCore/Extensions/Foundation/Dispatch and Foundation.swift b/Sources/OTCore/Extensions/Foundation/Dispatch and Foundation.swift index 5b8a382..6b6009c 100644 --- a/Sources/OTCore/Extensions/Foundation/Dispatch and Foundation.swift +++ b/Sources/OTCore/Extensions/Foundation/Dispatch and Foundation.swift @@ -1,5 +1,5 @@ // -// File.swift +// Dispatch and Foundation.swift // OTCore • https://github.com/orchetect/OTCore // @@ -7,46 +7,102 @@ import Foundation // imports Dispatch +// MARK: - QualityOfService / QoSClass + extension QualityOfService { + /// Returns the Dispatch framework `DispatchQoS.QoSClass` equivalent. public var dispatchQoSClass: DispatchQoS.QoSClass { + switch self { case .userInteractive: return .userInteractive + case .userInitiated: return .userInitiated + case .utility: return .utility + case .background: return .background + case .default: return .default + @unknown default: return .default } + } + } extension DispatchQoS.QoSClass { + /// Returns the Foundation framework `QualityOfService` equivalent. public var qualityOfService: QualityOfService { + switch self { case .userInteractive: return .userInteractive + case .userInitiated: return .userInitiated + case .utility: return .utility + case .background: return .background + case .default: return .default + case .unspecified: return .default + @unknown default: return .default } + + } + +} + +// MARK: - DispatchTimeInterval + +extension DispatchTimeInterval { + + /// **OTCore:** + /// Return the interval as a `TimeInterval` (floating-point seconds). + public var timeInterval: TimeInterval? { + + switch self { + case .seconds(let val): + return TimeInterval(val) + + case .milliseconds(let val): // ms + return TimeInterval(val) / 1_000 + + case .microseconds(let val): // µs + return TimeInterval(val) / 1_000_000 + + case .nanoseconds(let val): // ns + return TimeInterval(val) / 1_000_000_000 + + case .never: + //assertionFailure("Cannot convert 'never' to TimeInterval.") + return nil + + @unknown default: + assertionFailure("Unhandled DispatchTimeInterval case when attempting to convert to TimeInterval.") + return nil + + } + } + } #endif diff --git a/Tests/OTCoreTests/Extensions/Foundation/Dispatch and Foundation Tests.swift b/Tests/OTCoreTests/Extensions/Foundation/Dispatch and Foundation Tests.swift new file mode 100644 index 0000000..890e20d --- /dev/null +++ b/Tests/OTCoreTests/Extensions/Foundation/Dispatch and Foundation Tests.swift @@ -0,0 +1,36 @@ +// +// Dispatch and Foundation Tests.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if shouldTestCurrentPlatform + +import XCTest +@testable import OTCore + +class Extensions_Foundation_DispatchAndFoundation_Tests: XCTestCase { + + override func setUp() { super.setUp() } + override func tearDown() { super.tearDown() } + + // MARK: - DispatchTimeInterval + + func testDispatchTimeInterval_timeInterval() { + + + XCTAssertEqual(DispatchTimeInterval.seconds(2).timeInterval, 2.0) + + XCTAssertEqual(DispatchTimeInterval.milliseconds(250).timeInterval, 0.250) + + XCTAssertEqual(DispatchTimeInterval.microseconds(250).timeInterval, 0.000_250) + + XCTAssertEqual(DispatchTimeInterval.nanoseconds(250).timeInterval, 0.000_000_250) + + XCTAssertNil(DispatchTimeInterval.never.timeInterval) + + + } + +} + +#endif From 633912bad2b148608377e8ea03e5c3afa1235a68 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:21:38 -0800 Subject: [PATCH 05/31] `BasicOperationQueue` fixes --- .../Complex/AtomicBlockOperation.swift | 10 ++++- .../OperationQueue/BasicOperationQueue.swift | 38 ++++++++++++++++--- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index 294259e..bdc1fe1 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -215,11 +215,19 @@ open class AtomicBlockOperation: BasicOperation { } + private func removeObservers() { + + observers.removeAll() + + } + deinit { + setupBlock = nil // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time - observers.removeAll() + removeObservers() + } } diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 259452f..a7e3b2d 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -44,6 +44,8 @@ open class BasicOperationQueue: OperationQueue { // MARK: - Status + /// Operation queue status. + /// To observe changes to this value, supply a closure to the `statusHandler` property. public internal(set) var status: OperationQueueStatus = .idle { didSet { if status != oldValue { @@ -82,6 +84,8 @@ open class BasicOperationQueue: OperationQueue { updateFromOperationQueueType() + addObservers() + } // MARK: - Overrides @@ -173,6 +177,7 @@ open class BasicOperationQueue: OperationQueue { /// **OTCore:** /// Retain property observers. For safety, this array must be emptied on class deinit. private var observers: [NSKeyValueObservation] = [] + private func addObservers() { // self.progress.isFinished @@ -181,9 +186,9 @@ open class BasicOperationQueue: OperationQueue { progress.observe(\.isFinished, options: [.new]) { [weak self] _, change in guard let self = self else { return } - guard let newValue = change.newValue else { return } + //guard let newValue = change.newValue else { return } - if newValue { + if self.progress.isFinished { if self.resetProgressWhenFinished { self.progress.totalUnitCount = 0 } @@ -192,16 +197,31 @@ open class BasicOperationQueue: OperationQueue { } ) + // self.progress.fractionCompleted + + observers.append( + observe(\.operationCount, options: [.new]) + { [weak self] _, change in + guard let self = self else { return } + //guard let newValue = change.newValue else { return } + + guard !self.progress.isFinished else { return } + self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, + message: self.progress.localizedDescription) + } + ) + + // self.progress.fractionCompleted observers.append( progress.observe(\.fractionCompleted, options: [.new]) { [weak self] _, change in guard let self = self else { return } - guard let newValue = change.newValue else { return } + //guard let newValue = change.newValue else { return } guard !self.progress.isFinished else { return } - self.status = .inProgress(fractionCompleted: newValue, + self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, message: self.progress.localizedDescription) } ) @@ -231,9 +251,17 @@ open class BasicOperationQueue: OperationQueue { } + private func removeObservers() { + + observers.removeAll() + + } + deinit { + // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time - observers.removeAll() + removeObservers() + } } From e3867f4d6e8add38db56e5525a82340439996620 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:22:51 -0800 Subject: [PATCH 06/31] `XCTestCase`: Added `wait(for condition: timeout: polling:)` method --- Tests/OTCoreTests/Utilities.swift | 81 +++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 Tests/OTCoreTests/Utilities.swift diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift new file mode 100644 index 0000000..2935a36 --- /dev/null +++ b/Tests/OTCoreTests/Utilities.swift @@ -0,0 +1,81 @@ +// +// OTCoreTests.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if shouldTestCurrentPlatform + +import XCTest + +extension XCTestCase { + + /// **OTCore:** + /// Wait for a condition to be true, with a timeout period. + /// Polling defaults to every 10 milliseconds, but can be overridden. + public func wait( + for condition: @autoclosure () -> Bool, + timeout: TimeInterval, + polling: DispatchTimeInterval = .milliseconds(10) + ) { + + let inTime = Date() + let timeoutTime = inTime + timeout + let pollingPeriodMicroseconds = UInt32(polling.microseconds) + + var continueLooping = true + var timedOut = false + + while continueLooping { + if Date() >= timeoutTime { + continueLooping = false + timedOut = true + continue + } + + let conditionResult = condition() + continueLooping = !conditionResult + if !continueLooping { continue } + + usleep(pollingPeriodMicroseconds) + } + + if timedOut { + XCTFail("Timed out.") + return + } + + } + +} + +class Utilities_WaitForConditionTests: XCTestCase { + + func testWaitForCondition_True() { + + wait(for: true, timeout: 0.1) + + } + + func testWaitForCondition_False() { + + XCTExpectFailure() + wait(for: false, timeout: 0.1) + + } + + func testWaitForCondition() { + + var someString = "default string" + + DispatchQueue.global().async { + usleep(20_000) // 20 milliseconds + someString = "new string" + } + + wait(for: someString == "new string", timeout: 0.1) // 100ms + + } + +} + +#endif From ebc1f02d6881616b7492e635f2214e38442b8a12 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 16:26:27 -0800 Subject: [PATCH 07/31] Updated internal `usleep()` instances to use `sleep(_: TimeInterval)` --- .../Operation/Complex/AtomicBlockOperation.swift | 6 +++--- .../Extensions/Foundation/DispatchGroup Tests.swift | 7 ++++--- .../Threading/Operation/BlockOperation Tests.swift | 2 +- .../Operation/Closure/AsyncClosureOperation Tests.swift | 8 ++++---- .../Operation/Closure/ClosureOperation Tests.swift | 2 +- .../Closure/InteractiveAsyncClosureOperation Tests.swift | 8 ++++---- .../Closure/InteractiveClosureOperation Tests.swift | 2 +- .../Operation/Complex/AtomicBlockOperation Tests.swift | 6 +++--- .../Foundational/BasicAsyncOperation Tests.swift | 6 +++--- .../Operation/Foundational/BasicOperation Tests.swift | 2 +- .../OperationQueue/OperationQueue Extensions Tests.swift | 2 +- Tests/OTCoreTests/Utilities.swift | 3 ++- 12 files changed, 28 insertions(+), 26 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index bdc1fe1..b01586f 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -139,11 +139,11 @@ open class AtomicBlockOperation: BasicOperation { // this ensures that the operation runs synchronously // which mirrors the behavior of BlockOperation while !isFinished { - usleep(10_000) // 10 milliseconds + sleep(0.010) // 10ms - //Thread.sleep(forTimeInterval: 0.010) // 10 milliseconds + //Thread.sleep(forTimeInterval: 0.010) - //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) // 10 milliseconds + //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) } } diff --git a/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift b/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift index c458a32..9f120e5 100644 --- a/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift +++ b/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift @@ -33,7 +33,8 @@ class Extensions_Foundation_DispatchGroup_Tests: XCTestCase { var val = 0 DispatchGroup.sync(asyncOn: .global()) { g in - usleep(100_000) // 100 milliseconds + sleep(0.1) + val = 1 g.leave() } @@ -120,7 +121,7 @@ class Extensions_Foundation_DispatchGroup_Tests: XCTestCase { func testSyncReturnValueOnQueue() { let returnValue: Int = DispatchGroup.sync(asyncOn: .global()) { g in - usleep(100_000) // 100 milliseconds + sleep(0.1) g.leave(withValue: 1) } @@ -211,7 +212,7 @@ class Extensions_Foundation_DispatchGroup_Tests: XCTestCase { DispatchGroup.sync { g2 in DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(100)) { result = DispatchGroup.sync { g3 in - usleep(100_000) // 100 milliseconds + sleep(0.1) exp.fulfill() g3.leave(withValue: 2) } diff --git a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift index d0c1a16..f1dad5e 100644 --- a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift @@ -21,7 +21,7 @@ final class BlockOperation_Tests: XCTestCase { for val in 1...100 { // will multi-thread op.addExecutionBlock { - usleep(100_000) // milliseconds + sleep(0.1) self.arr.append(val) } } diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift index 5c1d773..59c41b7 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift @@ -100,12 +100,12 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { } op.start() - usleep(100_000) // 100 milliseconds + sleep(0.1) op.cancel() // cancel the operation directly (since we are not using an OperationQueue) wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - usleep(200_000) // give a little time for cleanup + sleep(0.2) // give a little time for cleanup // state XCTAssertFalse(op.isFinished) @@ -195,7 +195,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { // queue automatically starts the operation once it's added opQ.addOperation(op) - usleep(100_000) // 100 milliseconds + sleep(0.1) opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. wait(for: [mainBlockExp, completionBlockExp], timeout: 0.4) @@ -218,7 +218,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { XCTAssertFalse(opQ.progress.isIndeterminate) wait(for: [mainBlockFinishedExp], timeout: 0.7) - usleep(100_000) // 100 milliseconds + sleep(0.1) // state XCTAssertEqual(opQ.operationCount, 0) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift index bd14b15..15e2b06 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift @@ -184,7 +184,7 @@ final class Threading_ClosureOperation_Tests: XCTestCase { let op = ClosureOperation { self.mainCheck() - usleep(500_000) // 500 milliseconds + sleep(0.5) val = 1 } diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift index 06a7462..52c3756 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift @@ -91,7 +91,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { for i in 1...100 { // finishes in 20 seconds operation.progress.completedUnitCount = Int64(i) - usleep(200_000) // 200 milliseconds + sleep(0.2) // would call this once ore more throughout the operation if operation.mainShouldAbort() { return } @@ -105,7 +105,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { } op.start() - usleep(100_000) // 100 milliseconds + sleep(0.1) op.cancel() // cancel the operation directly (since we are not using an OperationQueue) wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) @@ -194,7 +194,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { for i in 1...100 { // finishes in 20 seconds operation.progress.completedUnitCount = Int64(i) - usleep(200_000) // 200 milliseconds + sleep(0.2) // would call this once ore more throughout the operation if operation.mainShouldAbort() { return } @@ -212,7 +212,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { // queue automatically starts the operation once it's added opQ.addOperation(op) - usleep(100_000) // 100 milliseconds + sleep(0.1) opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift index 4d54f6e..4032335 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift @@ -116,7 +116,7 @@ final class Threading_InteractiveClosureOperation_Tests: XCTestCase { let op = InteractiveClosureOperation { operation in mainBlockExp.fulfill() XCTAssertTrue(operation.isExecuting) - usleep(500_000) // 500 milliseconds + sleep(0.5) val = 1 } diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift index 600288e..010acd4 100644 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift @@ -249,7 +249,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { for val in 1...100 { // will take 1 second to complete op.addOperation { v in - usleep(10_000) // milliseconds + sleep(0.01) v.mutate { $0.append(val) } } } @@ -341,7 +341,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { for valueNum in 1...20 { let ref = subOp.addInteractiveOperation { op, v in if op.mainShouldAbort() { return } - usleep(200_000) + sleep(0.2) v.mutate { value in value.append(valueNum) } @@ -376,7 +376,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { DispatchQueue.global().async { mainOp.start() } - usleep(100_000) // 100 milliseconds + sleep(0.1) mainOp.cancel() wait(for: [completionBlockExp], timeout: 1) diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift index fe888ca..2a64c3e 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift @@ -70,7 +70,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { self.progress.completedUnitCount = Int64(opNum) - usleep(200_000) // 200 milliseconds + sleep(0.2) } self.completeOperation() @@ -148,7 +148,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { } op.start() - usleep(100_000) // 100 milliseconds + sleep(0.1) op.cancel() // cancel the operation directly (since we are not using an OperationQueue) wait(for: [completionBlockExp], timeout: 0.5) @@ -222,7 +222,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { // queue automatically starts the operation once it's added opQ.addOperation(op) - usleep(100_000) // 100 milliseconds + sleep(0.1) opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. wait(for: [completionBlockExp], timeout: 0.5) diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift index ee76e22..b7ad18f 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift @@ -63,7 +63,7 @@ final class Threading_BasicOperation_Tests: XCTestCase { // but it does nothing here since we're not asking this class to cancel if mainShouldAbort() { return } - usleep(500_000) // 500 milliseconds + sleep(0.5) val = valChangeTo completeOperation() diff --git a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift index 6046dcf..f147aed 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift @@ -22,7 +22,7 @@ final class Threading_OperationQueueExtensions_Tests: XCTestCase { var val = 0 opQ.addOperation { - usleep(100_000) // 100 milliseconds + sleep(0.1) val = 1 } diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift index 2935a36..775c48d 100644 --- a/Tests/OTCoreTests/Utilities.swift +++ b/Tests/OTCoreTests/Utilities.swift @@ -6,6 +6,7 @@ #if shouldTestCurrentPlatform import XCTest +import OTCore extension XCTestCase { @@ -68,7 +69,7 @@ class Utilities_WaitForConditionTests: XCTestCase { var someString = "default string" DispatchQueue.global().async { - usleep(20_000) // 20 milliseconds + sleep(0.02) someString = "new string" } From ad7b528938313b003b10627cc2c04c4776c34e2a Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 17:06:46 -0800 Subject: [PATCH 08/31] `BasicOperationQueue`: Added `status` unit tests --- .../BasicOperationQueue Tests.swift | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index 6a00115..f24ddaa 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -13,20 +13,20 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { /// Serial FIFO queue. func testOperationQueueType_serialFIFO() { - let oq = BasicOperationQueue(type: .serialFIFO) + let opQ = BasicOperationQueue(type: .serialFIFO) - XCTAssertEqual(oq.maxConcurrentOperationCount, 1) + XCTAssertEqual(opQ.maxConcurrentOperationCount, 1) } /// Automatic concurrency. func testOperationQueueType_automatic() { - let oq = BasicOperationQueue(type: .concurrentAutomatic) + let opQ = BasicOperationQueue(type: .concurrentAutomatic) - print(oq.maxConcurrentOperationCount) + print(opQ.maxConcurrentOperationCount) - XCTAssertEqual(oq.maxConcurrentOperationCount, + XCTAssertEqual(opQ.maxConcurrentOperationCount, OperationQueue.defaultMaxConcurrentOperationCount) } @@ -34,28 +34,70 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { /// Specific number of concurrent operations. func testOperationQueueType_specific() { - let oq = BasicOperationQueue(type: .concurrent(max: 2)) + let opQ = BasicOperationQueue(type: .concurrent(max: 2)) - print(oq.maxConcurrentOperationCount) + print(opQ.maxConcurrentOperationCount) - XCTAssertEqual(oq.maxConcurrentOperationCount, 2) + XCTAssertEqual(opQ.maxConcurrentOperationCount, 2) } func testLastAddedOperation() { - let oq = BasicOperationQueue(type: .serialFIFO) - oq.isSuspended = true - XCTAssertEqual(oq.lastAddedOperation, nil) + let opQ = BasicOperationQueue(type: .serialFIFO) + opQ.isSuspended = true + XCTAssertEqual(opQ.lastAddedOperation, nil) var op: Operation? = Operation() - oq.addOperation(op!) - XCTAssertEqual(oq.lastAddedOperation, op) + opQ.addOperation(op!) + XCTAssertEqual(opQ.lastAddedOperation, op) + // just FYI: op.isFinished == false here + // but we don't care since it doesn't affect this test op = nil - oq.isSuspended = false - oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(oq.lastAddedOperation, nil) + opQ.isSuspended = false + wait(for: opQ.lastAddedOperation == nil, timeout: 0.2) + + } + + func testStatus() { + + let opQ = BasicOperationQueue(type: .serialFIFO) + + opQ.statusHandler = { newStatus, oldStatus in + print(oldStatus, newStatus) + } + + XCTAssertEqual(opQ.status, .idle) + + let completionBlockExp = expectation(description: "Operation Completion") + + opQ.addOperation { + sleep(0.1) + completionBlockExp.fulfill() + } + + switch opQ.status { + case .inProgress(let fractionCompleted, let message): + XCTAssertEqual(fractionCompleted, 0.0) + _ = message // don't test message content, for now + default: + XCTFail() + } + + wait(for: [completionBlockExp], timeout: 0.3) + wait(for: opQ.operationCount == 0, timeout: 0.1) + wait(for: opQ.progress.isFinished, timeout: 0.1) + + XCTAssertEqual(opQ.status, .idle) + + opQ.isSuspended = true + + XCTAssertEqual(opQ.status, .paused) + + opQ.isSuspended = false + + XCTAssertEqual(opQ.status, .idle) } From 19087a10d894fa5c4fef1615839bd0c69e9a6c9a Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 17:07:30 -0800 Subject: [PATCH 09/31] Updated unit tests to use `wait(for:timeout:polling:)` instead of `sleep()` where possible --- .../Closure/AsyncClosureOperation Tests.swift | 2 +- ...nteractiveAsyncClosureOperation Tests.swift | 1 - .../InteractiveClosureOperation Tests.swift | 18 +++++++++++------- .../Complex/AtomicBlockOperation Tests.swift | 3 +++ .../BasicAsyncOperation Tests.swift | 1 - 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift index 59c41b7..5277260 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift @@ -218,7 +218,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { XCTAssertFalse(opQ.progress.isIndeterminate) wait(for: [mainBlockFinishedExp], timeout: 0.7) - sleep(0.1) + wait(for: opQ.operationCount == 0, timeout: 0.5) // state XCTAssertEqual(opQ.operationCount, 0) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift index 52c3756..fcd1339 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift @@ -211,7 +211,6 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added opQ.addOperation(op) - sleep(0.1) opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift index 4032335..49b9be5 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift @@ -32,10 +32,11 @@ final class Threading_InteractiveClosureOperation_Tests: XCTestCase { op.start() wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - + + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -61,11 +62,12 @@ final class Threading_InteractiveClosureOperation_Tests: XCTestCase { } wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - + + // state XCTAssertTrue(op.isReady) + XCTAssertFalse(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) } @@ -97,10 +99,11 @@ final class Threading_InteractiveClosureOperation_Tests: XCTestCase { wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) XCTAssertEqual(opQ.operationCount, 0) - + + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) } @@ -128,9 +131,10 @@ final class Threading_InteractiveClosureOperation_Tests: XCTestCase { XCTAssertEqual(val, 1) + // state + XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) wait(for: [mainBlockExp, completionBlockExp], timeout: 2) diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift index 010acd4..7f78364 100644 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift @@ -51,6 +51,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) + // state XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) @@ -92,6 +93,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { XCTAssertEqual(op.value.count, 100) XCTAssert(Array(1...100).allSatisfy(op.value.contains)) + // state XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) @@ -177,6 +179,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { XCTAssertEqual(op.value.count, 100) XCTAssert(Array(1...100).allSatisfy(op.value.contains)) + // state XCTAssertTrue(op.isFinished) XCTAssertFalse(op.isCancelled) XCTAssertFalse(op.isExecuting) diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift index 2a64c3e..6e75839 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift @@ -221,7 +221,6 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { opQ.progress.totalUnitCount += 1 // queue automatically starts the operation once it's added opQ.addOperation(op) - sleep(0.1) opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. From ec8a6796ae0b58bbdc2c04619976a6ccef8a786c Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 17:58:41 -0800 Subject: [PATCH 10/31] `BasicOperationQueue`: Improved reliability of `status` transition to `.idle` --- .../OperationQueue/BasicOperationQueue.swift | 22 +++- .../AtomicOperationQueue Tests.swift | 115 +++++++++++------- 2 files changed, 85 insertions(+), 52 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index a7e3b2d..0f4d260 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -197,7 +197,7 @@ open class BasicOperationQueue: OperationQueue { } ) - // self.progress.fractionCompleted + // self.operationCount observers.append( observe(\.operationCount, options: [.new]) @@ -205,9 +205,14 @@ open class BasicOperationQueue: OperationQueue { guard let self = self else { return } //guard let newValue = change.newValue else { return } + guard !self.isSuspended else { return } guard !self.progress.isFinished else { return } - self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, - message: self.progress.localizedDescription) + if self.operationCount > 0 { + self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, + message: self.progress.localizedDescription) + } else { + self.status = .idle + } } ) @@ -220,7 +225,12 @@ open class BasicOperationQueue: OperationQueue { guard let self = self else { return } //guard let newValue = change.newValue else { return } - guard !self.progress.isFinished else { return } + guard !self.isSuspended else { return } + guard !self.progress.isFinished, + self.operationCount > 0 else { + self.status = .idle + return + } self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, message: self.progress.localizedDescription) } @@ -232,9 +242,9 @@ open class BasicOperationQueue: OperationQueue { observe(\.isSuspended, options: [.new]) { [weak self] _, change in guard let self = self else { return } - guard let newValue = change.newValue else { return } + //guard let newValue = change.newValue else { return } - if newValue { + if self.isSuspended { self.status = .paused } else { if self.operationCount > 0 { diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index 72b4900..e61a36f 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -13,86 +13,108 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { /// Serial FIFO queue. func testOp_serialFIFO_Run() { - let oq = AtomicOperationQueue(type: .serialFIFO, - initialMutableValue: [Int]()) + let opQ = AtomicOperationQueue(type: .serialFIFO, + initialMutableValue: [Int]()) for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } + opQ.addOperation { $0.mutate { $0.append(val) } } } - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) + wait(for: opQ.status == .idle, timeout: 0.5) + //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) + //XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) + XCTAssertEqual(opQ.sharedMutableValue.count, 100) + XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertFalse(opQ.isSuspended) + XCTAssertEqual(opQ.status, .idle) } /// Concurrent automatic threading. Run it. func testOp_concurrentAutomatic_Run() { - let oq = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) + let opQ = AtomicOperationQueue(type: .concurrentAutomatic, + initialMutableValue: [Int]()) for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } + opQ.addOperation { $0.mutate { $0.append(val) } } } - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) + wait(for: opQ.status == .idle, timeout: 0.5) + //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) + //XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.sharedMutableValue.count, 100) + XCTAssertEqual(opQ.sharedMutableValue.count, 100) // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) + XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertFalse(opQ.isSuspended) + XCTAssertEqual(opQ.status, .idle) } - /// Concurrent automatic threading. Do not run it. + /// Concurrent automatic threading. Do not run it. Check status. Run it. Check status. func testOp_concurrentAutomatic_NotRun() { - let oq = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) + let opQ = AtomicOperationQueue(type: .concurrentAutomatic, + initialMutableValue: [Int]()) + + opQ.isSuspended = true - oq.isSuspended = true + XCTAssertEqual(opQ.status, .paused) for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } + opQ.addOperation { $0.mutate { $0.append(val) } } } - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) + XCTAssertEqual(opQ.status, .paused) + + let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(200)) XCTAssertEqual(timeoutResult, .timedOut) - XCTAssertEqual(oq.sharedMutableValue, []) - XCTAssertEqual(oq.operationCount, 100) - XCTAssertTrue(oq.isSuspended) + XCTAssertEqual(opQ.sharedMutableValue, []) + XCTAssertEqual(opQ.operationCount, 100) + XCTAssertTrue(opQ.isSuspended) + + wait(for: opQ.status == .paused, timeout: 0.1) + XCTAssertEqual(opQ.status, .paused) + + opQ.isSuspended = false + + wait(for: opQ.status == .idle, timeout: 0.5) + XCTAssertEqual(opQ.status, .idle) + + XCTAssertEqual(opQ.sharedMutableValue.count, 100) + // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used + XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) } /// Concurrent automatic threading. Run it. func testOp_concurrentSpecific_Run() { - let oq = AtomicOperationQueue(type: .concurrent(max: 10), - initialMutableValue: [Int]()) + let opQ = AtomicOperationQueue(type: .concurrent(max: 10), + initialMutableValue: [Int]()) for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } + opQ.addOperation { $0.mutate { $0.append(val) } } } - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) + wait(for: opQ.status == .idle, timeout: 0.5) + //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) + //XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.sharedMutableValue.count, 100) + XCTAssertEqual(opQ.sharedMutableValue.count, 100) // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) + XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertFalse(opQ.isSuspended) + XCTAssertEqual(opQ.status, .idle) } @@ -100,33 +122,34 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { /// Test the behavior of `addOperations()`. It should add operations in array order. func testOp_serialFIFO_AddOperations_Run() { - let oq = AtomicOperationQueue(type: .serialFIFO, - initialMutableValue: [Int]()) + let opQ = AtomicOperationQueue(type: .serialFIFO, + initialMutableValue: [Int]()) var ops: [Operation] = [] // first generate operation objects for val in 1...50 { - let op = oq.createOperation { $0.mutate { $0.append(val) } } + let op = opQ.createOperation { $0.mutate { $0.append(val) } } ops.append(op) } for val in 51...100 { - let op = oq.createInteractiveOperation { _, v in + let op = opQ.createInteractiveOperation { _, v in v.mutate { $0.append(val) } } ops.append(op) } // then addOperations() with all 100 operations - oq.addOperations(ops, waitUntilFinished: false) + opQ.addOperations(ops, waitUntilFinished: false) - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) + let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) + XCTAssertEqual(opQ.sharedMutableValue.count, 100) + XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) + XCTAssertEqual(opQ.operationCount, 0) + XCTAssertFalse(opQ.isSuspended) + XCTAssertEqual(opQ.status, .idle) } From 1b59539f0998a36d41802f6b42f9f945d29a1f52 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 18:00:03 -0800 Subject: [PATCH 11/31] ... --- .../OTCore/Threading/OperationQueue/BasicOperationQueue.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 0f4d260..1338a3d 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -216,7 +216,6 @@ open class BasicOperationQueue: OperationQueue { } ) - // self.progress.fractionCompleted observers.append( From 07486ea1ec22d3895308d05a50094d0484ca3f69 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 18:08:14 -0800 Subject: [PATCH 12/31] `BasicOperationQueue`: added unit tests for `resetProgressWhenFinished` --- .../BasicOperationQueue Tests.swift | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index f24ddaa..139bd68 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -60,6 +60,38 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { } + func testResetProgressWhenFinished_False() { + + let opQ = BasicOperationQueue(type: .serialFIFO, + resetProgressWhenFinished: false) + + for _ in 1...10 { + opQ.addOperation { } + } + + wait(for: opQ.status == .idle, timeout: 0.2) + wait(for: opQ.operationCount == 0, timeout: 0.2) + + XCTAssertEqual(opQ.progress.totalUnitCount, 10) + + } + + func testResetProgressWhenFinished_True() { + + let opQ = BasicOperationQueue(type: .serialFIFO, + resetProgressWhenFinished: true) + + for _ in 1...10 { + opQ.addOperation { } + } + + wait(for: opQ.status == .idle, timeout: 0.2) + + wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.2) + XCTAssertEqual(opQ.progress.totalUnitCount, 0) + + } + func testStatus() { let opQ = BasicOperationQueue(type: .serialFIFO) From f137abafe2c07ff300953e08012df030ef3a832e Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 18:19:22 -0800 Subject: [PATCH 13/31] Fixed builds on iOS, tvOS and watchOS --- .../OTCore/Threading/OperationQueue/BasicOperationQueue.swift | 2 +- .../Operation/Closure/AsyncClosureOperation Tests.swift | 2 ++ .../Threading/Operation/Closure/ClosureOperation Tests.swift | 1 + .../Closure/InteractiveAsyncClosureOperation Tests.swift | 2 ++ .../Operation/Complex/AtomicBlockOperation Tests.swift | 1 + .../Operation/Foundational/BasicAsyncOperation Tests.swift | 2 ++ .../Threading/Operation/Foundational/BasicOperation Tests.swift | 1 + .../Threading/OperationQueue/AtomicOperationQueue Tests.swift | 2 +- .../Threading/OperationQueue/BasicOperationQueue Tests.swift | 2 +- 9 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 1338a3d..5713569 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -46,7 +46,7 @@ open class BasicOperationQueue: OperationQueue { /// Operation queue status. /// To observe changes to this value, supply a closure to the `statusHandler` property. - public internal(set) var status: OperationQueueStatus = .idle { + @Atomic public internal(set) var status: OperationQueueStatus = .idle { didSet { if status != oldValue { statusHandler?(status, oldValue) diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift index 5277260..e8df6d6 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift @@ -121,6 +121,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue() { let opQ = OperationQueue() @@ -163,6 +164,7 @@ final class Threading_AsyncClosureOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue_Cancel() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift index 15e2b06..b3c835b 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift @@ -127,6 +127,7 @@ final class Threading_ClosureOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift index fcd1339..0869429 100644 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift @@ -124,6 +124,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue() { let opQ = OperationQueue() @@ -171,6 +172,7 @@ final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue_Cancel() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift index 7f78364..cab2b69 100644 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift @@ -187,6 +187,7 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testOp_concurrentAutomatic_Queue() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift index 6e75839..52dff19 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift @@ -167,6 +167,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue() { let opQ = OperationQueue() @@ -205,6 +206,7 @@ final class Threading_BasicAsyncOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue_Cancel() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift index b7ad18f..69dd193 100644 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift @@ -158,6 +158,7 @@ final class Threading_BasicOperation_Tests: XCTestCase { } /// Test in the context of an OperationQueue. Run is implicit. + @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) func testQueue() { let opQ = OperationQueue() diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index e61a36f..1fbb313 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -85,7 +85,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { opQ.isSuspended = false - wait(for: opQ.status == .idle, timeout: 0.5) + wait(for: opQ.status == .idle, timeout: 1.0) XCTAssertEqual(opQ.status, .idle) XCTAssertEqual(opQ.sharedMutableValue.count, 100) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index 139bd68..e7f6068 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -87,7 +87,7 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { wait(for: opQ.status == .idle, timeout: 0.2) - wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.2) + wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) XCTAssertEqual(opQ.progress.totalUnitCount, 0) } From d5e969d6a15f9a4bc5e908fa2f10bcd6ef2ec4d9 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Thu, 10 Feb 2022 18:28:54 -0800 Subject: [PATCH 14/31] Fixed Xcode 12.5 build --- Tests/OTCoreTests/Utilities.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift index 775c48d..2d40f22 100644 --- a/Tests/OTCoreTests/Utilities.swift +++ b/Tests/OTCoreTests/Utilities.swift @@ -57,12 +57,16 @@ class Utilities_WaitForConditionTests: XCTestCase { } + #if swift(>=5.4) + /// `XCTExpectFailure()` is only available in Xcode 12.5 or later. Swift 5.4 shipped in Xcode 12.5. func testWaitForCondition_False() { XCTExpectFailure() + wait(for: false, timeout: 0.1) } + #endif func testWaitForCondition() { From f350c87e4099af510ebe9c25c2880a9d1edc2985 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 11 Feb 2022 15:39:39 -0800 Subject: [PATCH 15/31] Made updating `swiftSettings` in the package manifest append instead of replace --- Package.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 8352d3e..0588808 100644 --- a/Package.swift +++ b/Package.swift @@ -39,9 +39,15 @@ let package = Package( ) func addShouldTestFlag() { + var swiftSettings = package.targets + .first(where: { $0.name == "OTCoreTests" })? + .swiftSettings ?? [] + + swiftSettings.append(.define("shouldTestCurrentPlatform")) + package.targets .first(where: { $0.name == "OTCoreTests" })? - .swiftSettings = [.define("shouldTestCurrentPlatform")] + .swiftSettings = swiftSettings } // Swift version in Xcode 12.5.1 which introduced watchOS testing From ed793a51e9201d2b81c093aa53d39ad704b1cbd4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 11 Feb 2022 15:41:04 -0800 Subject: [PATCH 16/31] Refactored KVO observers to read from property directly and not use `change.newValue` --- .../Complex/AtomicBlockOperation.swift | 41 +++++++------- .../OperationQueue/BasicOperationQueue.swift | 55 +++++++++---------- 2 files changed, 45 insertions(+), 51 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index b01586f..0633108 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -159,36 +159,21 @@ open class AtomicBlockOperation: BasicOperation { observers.append( observe(\.isCancelled, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } - guard let newValue = change.newValue else { return } - if newValue { + if self.isCancelled { self.operationQueue.cancelAllOperations() self.completeOperation(dueToCancellation: true) } } ) - // self.operationQueue.operationCount - - observers.append( - operationQueue.observe(\.operationCount, options: [.new]) - { [weak self] _, change in - guard let self = self else { return } - guard let newValue = change.newValue else { return } - - if newValue == 0 { - self.completeOperation() - } - } - ) - // self.qualityOfService observers.append( observe(\.qualityOfService, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } // for some reason, change.newValue is nil here. so just read from the property directly. @@ -199,15 +184,27 @@ open class AtomicBlockOperation: BasicOperation { } ) + // self.operationQueue.operationCount + + observers.append( + operationQueue.observe(\.operationCount, options: [.new]) + { [weak self] _, _ in + guard let self = self else { return } + + if self.operationQueue.operationCount == 0 { + self.completeOperation() + } + } + ) + // self.operationQueue.progress.isFinished observers.append( operationQueue.progress.observe(\.isFinished, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } - guard let newValue = change.newValue else { return } - if newValue { + if self.operationQueue.progress.isFinished { self.completeOperation() } } @@ -217,8 +214,8 @@ open class AtomicBlockOperation: BasicOperation { private func removeObservers() { + observers.forEach { $0.invalidate() } // for extra safety, invalidate them first observers.removeAll() - } deinit { diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 5713569..b445f34 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -180,13 +180,34 @@ open class BasicOperationQueue: OperationQueue { private func addObservers() { + // self.isSuspended + + observers.append( + observe(\.isSuspended, options: [.new]) + { [weak self] _, _ in + guard let self = self else { return } + + if self.isSuspended { + self.status = .paused + } else { + if self.operationCount > 0 { + self.status = .inProgress( + fractionCompleted: self.progress.fractionCompleted, + message: self.progress.localizedDescription + ) + } else { + self.status = .idle + } + } + } + ) + // self.progress.isFinished observers.append( progress.observe(\.isFinished, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } - //guard let newValue = change.newValue else { return } if self.progress.isFinished { if self.resetProgressWhenFinished { @@ -201,9 +222,8 @@ open class BasicOperationQueue: OperationQueue { observers.append( observe(\.operationCount, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } - //guard let newValue = change.newValue else { return } guard !self.isSuspended else { return } guard !self.progress.isFinished else { return } @@ -220,9 +240,8 @@ open class BasicOperationQueue: OperationQueue { observers.append( progress.observe(\.fractionCompleted, options: [.new]) - { [weak self] _, change in + { [weak self] _, _ in guard let self = self else { return } - //guard let newValue = change.newValue else { return } guard !self.isSuspended else { return } guard !self.progress.isFinished, @@ -235,33 +254,11 @@ open class BasicOperationQueue: OperationQueue { } ) - // self.isSuspended - - observers.append( - observe(\.isSuspended, options: [.new]) - { [weak self] _, change in - guard let self = self else { return } - //guard let newValue = change.newValue else { return } - - if self.isSuspended { - self.status = .paused - } else { - if self.operationCount > 0 { - self.status = .inProgress( - fractionCompleted: self.progress.fractionCompleted, - message: self.progress.localizedDescription - ) - } else { - self.status = .idle - } - } - } - ) - } private func removeObservers() { + observers.forEach { $0.invalidate() } // for extra safety, invalidate them first observers.removeAll() } From b3760dd233dd10650ee7220358bb90c83ca6466a Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Fri, 11 Feb 2022 15:41:21 -0800 Subject: [PATCH 17/31] Minor unit test fixes --- .../Foundation/DispatchGroup Tests.swift | 2 +- .../OperationQueue Extensions Tests.swift | 25 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift b/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift index 9f120e5..4d4900b 100644 --- a/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift +++ b/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift @@ -210,7 +210,7 @@ class Extensions_Foundation_DispatchGroup_Tests: XCTestCase { DispatchGroup.sync { g1 in DispatchQueue.global().async { DispatchGroup.sync { g2 in - DispatchQueue.global().asyncAfter(deadline: .now() + .milliseconds(100)) { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) { result = DispatchGroup.sync { g3 in sleep(0.1) exp.fulfill() diff --git a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift index f147aed..90a6606 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift @@ -8,26 +8,28 @@ import OTCore import XCTest -final class Threading_OperationQueueExtensions_Tests: XCTestCase { +final class Threading_OperationQueueExtensions_Success_Tests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } + @Atomic fileprivate var val = 0 + func testWaitUntilAllOperationsAreFinished_Timeout_Success() { let opQ = OperationQueue() opQ.maxConcurrentOperationCount = 1 // serial opQ.isSuspended = true - var val = 0 + val = 0 opQ.addOperation { sleep(0.1) - val = 1 + self.val = 1 } opQ.isSuspended = false - let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(5)) + let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) XCTAssertEqual(timeoutResult, .success) XCTAssertEqual(opQ.operationCount, 0) @@ -35,17 +37,26 @@ final class Threading_OperationQueueExtensions_Tests: XCTestCase { } +} + +final class Threading_OperationQueueExtensions_TimedOut_Tests: XCTestCase { + + override func setUp() { super.setUp() } + override func tearDown() { super.tearDown() } + + @Atomic fileprivate var val = 0 + func testWaitUntilAllOperationsAreFinished_Timeout_TimedOut() { let opQ = OperationQueue() opQ.maxConcurrentOperationCount = 1 // serial opQ.isSuspended = true - var val = 0 + val = 0 opQ.addOperation { - sleep(5) // 5 seconds - val = 1 + sleep(1) + self.val = 1 } opQ.isSuspended = false From 6ae1bf6ad19337cd63fa431f6e34cf4d18183cc4 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 14:39:36 -0800 Subject: [PATCH 18/31] Fixed potential crash on macOS Catalina in KVO implementation --- .../Complex/AtomicBlockOperation.swift | 41 ++++++------ .../Foundational/BasicOperation.swift | 4 +- .../OperationQueue/AtomicOperationQueue.swift | 12 +++- .../OperationQueue/BasicOperationQueue.swift | 63 ++++++++++--------- 4 files changed, 69 insertions(+), 51 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index 0633108..1ca2613 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -56,7 +56,7 @@ open class AtomicBlockOperation: BasicOperation { operationQueue.operationQueueType } - private let operationQueue: AtomicOperationQueue + private let operationQueue: AtomicOperationQueue! /// **OTCore:** /// Stores a weak reference to the last `Operation` added to the internal operation queue. If the operation is complete and the queue is empty, this may return `nil`. @@ -96,25 +96,27 @@ open class AtomicBlockOperation: BasicOperation { public init(type operationQueueType: OperationQueueType, initialMutableValue: T, + qualityOfService: QualityOfService? = nil, resetProgressWhenFinished: Bool = false, statusHandler: BasicOperationQueue.StatusHandler? = nil) { // assign properties operationQueue = AtomicOperationQueue( type: operationQueueType, - initialMutableValue: initialMutableValue, + qualityOfService: qualityOfService, + initiallySuspended: true, resetProgressWhenFinished: resetProgressWhenFinished, + initialMutableValue: initialMutableValue, statusHandler: statusHandler ) // super super.init() - // set up queue - - operationQueue.isSuspended = true - - operationQueue.qualityOfService = qualityOfService + if let qualityOfService = qualityOfService { + self.qualityOfService = qualityOfService + self.operationQueue.qualityOfService = qualityOfService + } // set up observers addObservers() @@ -159,11 +161,11 @@ open class AtomicBlockOperation: BasicOperation { observers.append( observe(\.isCancelled, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, operationQueue] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if self.isCancelled { - self.operationQueue.cancelAllOperations() + operationQueue?.cancelAllOperations() self.completeOperation(dueToCancellation: true) } } @@ -173,14 +175,14 @@ open class AtomicBlockOperation: BasicOperation { observers.append( observe(\.qualityOfService, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, operationQueue] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! // for some reason, change.newValue is nil here. so just read from the property directly. // guard let newValue = change.newValue else { return } // propagate to operation queue - self.operationQueue.qualityOfService = self.qualityOfService + operationQueue?.qualityOfService = self.qualityOfService } ) @@ -188,23 +190,24 @@ open class AtomicBlockOperation: BasicOperation { observers.append( operationQueue.observe(\.operationCount, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, operationQueue] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - if self.operationQueue.operationCount == 0 { + if operationQueue?.operationCount == 0 { self.completeOperation() } } ) // self.operationQueue.progress.isFinished + // (NSProgress docs state that isFinished is KVO-observable) observers.append( operationQueue.progress.observe(\.isFinished, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, operationQueue] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - if self.operationQueue.progress.isFinished { + if operationQueue?.progress.isFinished == true { self.completeOperation() } } diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift index 4a4c125..f5a4465 100644 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift +++ b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift @@ -47,11 +47,12 @@ open class BasicOperation: Operation, ProgressReporting { /// **OTCore:** /// Progress object representing progress of the operation. - @Atomic public var progress: Progress = .init(totalUnitCount: 1) + public private(set) var progress: Progress = .init(totalUnitCount: 1) // MARK: - KVO // adding KVO compliance + @objc dynamic public final override var isExecuting: Bool { _isExecuting } @Atomic private var _isExecuting = false { willSet { willChangeValue(for: \.isExecuting) } @@ -59,6 +60,7 @@ open class BasicOperation: Operation, ProgressReporting { } // adding KVO compliance + @objc dynamic public final override var isFinished: Bool { _isFinished } @Atomic private var _isFinished = false { willSet { willChangeValue(for: \.isFinished) } diff --git a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift index 15eea07..20874fb 100644 --- a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift @@ -21,8 +21,10 @@ open class AtomicOperationQueue: BasicOperationQueue { public init( type operationQueueType: OperationQueueType = .concurrentAutomatic, - initialMutableValue: T, + qualityOfService: QualityOfService? = nil, + initiallySuspended: Bool = false, resetProgressWhenFinished: Bool = false, + initialMutableValue: T, statusHandler: BasicOperationQueue.StatusHandler? = nil ) { @@ -32,6 +34,14 @@ open class AtomicOperationQueue: BasicOperationQueue { resetProgressWhenFinished: resetProgressWhenFinished, statusHandler: statusHandler) + if let qualityOfService = qualityOfService { + self.qualityOfService = qualityOfService + } + + if initiallySuspended { + isSuspended = true + } + } // MARK: - Shared Mutable Value Methods diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index b445f34..46e1d84 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -66,6 +66,7 @@ open class BasicOperationQueue: OperationQueue { @Atomic private var _progress: Progress = .init() @available(macOS 10.9, iOS 7.0, tvOS 9.0, watchOS 2.0, *) + @objc dynamic public override final var progress: Progress { _progress } // MARK: - Init @@ -184,16 +185,16 @@ open class BasicOperationQueue: OperationQueue { observers.append( observe(\.isSuspended, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, progress] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if self.isSuspended { self.status = .paused } else { if self.operationCount > 0 { self.status = .inProgress( - fractionCompleted: self.progress.fractionCompleted, - message: self.progress.localizedDescription + fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription ) } else { self.status = .idle @@ -202,34 +203,18 @@ open class BasicOperationQueue: OperationQueue { } ) - // self.progress.isFinished - - observers.append( - progress.observe(\.isFinished, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } - - if self.progress.isFinished { - if self.resetProgressWhenFinished { - self.progress.totalUnitCount = 0 - } - self.status = .idle - } - } - ) - // self.operationCount observers.append( observe(\.operationCount, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, progress] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! guard !self.isSuspended else { return } - guard !self.progress.isFinished else { return } + guard !progress.isFinished else { return } if self.operationCount > 0 { - self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, - message: self.progress.localizedDescription) + self.status = .inProgress(fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription) } else { self.status = .idle } @@ -237,20 +222,38 @@ open class BasicOperationQueue: OperationQueue { ) // self.progress.fractionCompleted + // (NSProgress docs state that fractionCompleted is KVO-observable) observers.append( progress.observe(\.fractionCompleted, options: [.new]) - { [weak self] _, _ in - guard let self = self else { return } + { [self, progress] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! guard !self.isSuspended else { return } - guard !self.progress.isFinished, + guard !progress.isFinished, self.operationCount > 0 else { self.status = .idle return } - self.status = .inProgress(fractionCompleted: self.progress.fractionCompleted, - message: self.progress.localizedDescription) + self.status = .inProgress(fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription) + } + ) + + // self.progress.isFinished + // (NSProgress docs state that isFinished is KVO-observable) + + observers.append( + progress.observe(\.isFinished, options: [.new]) + { [self, progress] _, _ in + // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! + + if progress.isFinished { + if self.resetProgressWhenFinished { + progress.totalUnitCount = 0 + } + self.status = .idle + } } ) From 07f02f6d81c72e74d372d3c10e88342340e01acb Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 19:33:12 -0800 Subject: [PATCH 19/31] Relaxed timeouts for some unit tests --- .../BasicOperationQueue Tests.swift | 16 ++++++++-------- Tests/OTCoreTests/Utilities.swift | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index e7f6068..2223443 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -56,7 +56,7 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { op = nil opQ.isSuspended = false - wait(for: opQ.lastAddedOperation == nil, timeout: 0.2) + wait(for: opQ.lastAddedOperation == nil, timeout: 0.5) } @@ -69,8 +69,8 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { opQ.addOperation { } } - wait(for: opQ.status == .idle, timeout: 0.2) - wait(for: opQ.operationCount == 0, timeout: 0.2) + wait(for: opQ.status == .idle, timeout: 0.5) + wait(for: opQ.operationCount == 0, timeout: 0.5) XCTAssertEqual(opQ.progress.totalUnitCount, 10) @@ -85,9 +85,9 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { opQ.addOperation { } } - wait(for: opQ.status == .idle, timeout: 0.2) + wait(for: opQ.status == .idle, timeout: 0.5) - wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) + wait(for: opQ.progress.totalUnitCount == 0, timeout: 1.0) XCTAssertEqual(opQ.progress.totalUnitCount, 0) } @@ -117,9 +117,9 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { XCTFail() } - wait(for: [completionBlockExp], timeout: 0.3) - wait(for: opQ.operationCount == 0, timeout: 0.1) - wait(for: opQ.progress.isFinished, timeout: 0.1) + wait(for: [completionBlockExp], timeout: 0.5) + wait(for: opQ.operationCount == 0, timeout: 0.5) + wait(for: opQ.progress.isFinished, timeout: 0.5) XCTAssertEqual(opQ.status, .idle) diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift index 2d40f22..7ccd115 100644 --- a/Tests/OTCoreTests/Utilities.swift +++ b/Tests/OTCoreTests/Utilities.swift @@ -77,7 +77,7 @@ class Utilities_WaitForConditionTests: XCTestCase { someString = "new string" } - wait(for: someString == "new string", timeout: 0.1) // 100ms + wait(for: someString == "new string", timeout: 0.3) } From ec8527947381344412114b675abb95effa08e80b Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 19:33:36 -0800 Subject: [PATCH 20/31] Set GitHub CI scheme to random unit test execution order --- .swiftpm/xcode/xcshareddata/xcschemes/OTCore-CI.xcscheme | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/OTCore-CI.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/OTCore-CI.xcscheme index 322c1b1..cbedc1f 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/OTCore-CI.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/OTCore-CI.xcscheme @@ -44,7 +44,8 @@ codeCoverageEnabled = "YES"> + skipped = "NO" + testExecutionOrdering = "random"> Date: Sat, 12 Feb 2022 22:53:13 -0800 Subject: [PATCH 21/31] `BasicOperationQueue` / `AtomicBlockOperation` improvements --- .../Complex/AtomicBlockOperation.swift | 10 ++--- .../OperationQueue/BasicOperationQueue.swift | 41 +++++++++++-------- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index 1ca2613..01a8e4d 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -164,9 +164,9 @@ open class AtomicBlockOperation: BasicOperation { { [self, operationQueue] _, _ in // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - if self.isCancelled { + if isCancelled { operationQueue?.cancelAllOperations() - self.completeOperation(dueToCancellation: true) + completeOperation(dueToCancellation: true) } } ) @@ -182,7 +182,7 @@ open class AtomicBlockOperation: BasicOperation { // guard let newValue = change.newValue else { return } // propagate to operation queue - operationQueue?.qualityOfService = self.qualityOfService + operationQueue?.qualityOfService = qualityOfService } ) @@ -194,7 +194,7 @@ open class AtomicBlockOperation: BasicOperation { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if operationQueue?.operationCount == 0 { - self.completeOperation() + completeOperation() } } ) @@ -208,7 +208,7 @@ open class AtomicBlockOperation: BasicOperation { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if operationQueue?.progress.isFinished == true { - self.completeOperation() + completeOperation() } } ) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 46e1d84..b49f5d8 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -188,16 +188,16 @@ open class BasicOperationQueue: OperationQueue { { [self, progress] _, _ in // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - if self.isSuspended { - self.status = .paused + if isSuspended { + status = .paused } else { - if self.operationCount > 0 { - self.status = .inProgress( + if operationCount > 0 { + status = .inProgress( fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription ) } else { - self.status = .idle + setStatusIdle() } } } @@ -210,13 +210,13 @@ open class BasicOperationQueue: OperationQueue { { [self, progress] _, _ in // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - guard !self.isSuspended else { return } + guard !isSuspended else { return } guard !progress.isFinished else { return } if self.operationCount > 0 { - self.status = .inProgress(fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription) + status = .inProgress(fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription) } else { - self.status = .idle + setStatusIdle() } } ) @@ -229,14 +229,14 @@ open class BasicOperationQueue: OperationQueue { { [self, progress] _, _ in // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - guard !self.isSuspended else { return } + guard !isSuspended else { return } guard !progress.isFinished, - self.operationCount > 0 else { - self.status = .idle + operationCount > 0 else { + setStatusIdle() return } - self.status = .inProgress(fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription) + status = .inProgress(fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription) } ) @@ -249,10 +249,7 @@ open class BasicOperationQueue: OperationQueue { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if progress.isFinished { - if self.resetProgressWhenFinished { - progress.totalUnitCount = 0 - } - self.status = .idle + setStatusIdle() } } ) @@ -266,6 +263,14 @@ open class BasicOperationQueue: OperationQueue { } + /// Only call as a result of the queue emptying + private func setStatusIdle() { + if resetProgressWhenFinished { + progress.totalUnitCount = 0 + } + status = .idle + } + deinit { // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time From 539a3f664e540c88d10cbd635946a8a9fa8a0bd7 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 22:59:13 -0800 Subject: [PATCH 22/31] Updated unit tests --- .../BasicOperationQueue Tests.swift | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index 2223443..21c032e 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -82,12 +82,21 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { resetProgressWhenFinished: true) for _ in 1...10 { - opQ.addOperation { } + opQ.addOperation { sleep(0.1) } } - wait(for: opQ.status == .idle, timeout: 0.5) + XCTAssertEqual(opQ.progress.totalUnitCount, 10) + + switch opQ.status { + case .inProgress(fractionCompleted: _, message: _): + break // correct + default: + XCTFail() + } + + wait(for: opQ.status == .idle, timeout: 1.5) - wait(for: opQ.progress.totalUnitCount == 0, timeout: 1.0) + wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) XCTAssertEqual(opQ.progress.totalUnitCount, 0) } From f5c2d118c2e77a39b6e8cf4549c5a546ad0608db Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 23:02:11 -0800 Subject: [PATCH 23/31] Unit tests updated --- .../AtomicOperationQueue Tests.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index 1fbb313..cd4265e 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -153,6 +153,33 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { } + /// NOTE: this test similar to one in: BasicOperationQueue Tests.swift + func testResetProgressWhenFinished_True() { + + let opQ = AtomicOperationQueue(type: .serialFIFO, + resetProgressWhenFinished: true, + initialMutableValue: 0) // value doesn't matter + + for _ in 1...10 { + opQ.addInteractiveOperation { _,_ in sleep(0.1) } + } + + XCTAssertEqual(opQ.progress.totalUnitCount, 10) + + switch opQ.status { + case .inProgress(fractionCompleted: _, message: _): + break // correct + default: + XCTFail() + } + + wait(for: opQ.status == .idle, timeout: 1.5) + + wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) + XCTAssertEqual(opQ.progress.totalUnitCount, 0) + + } + } #endif From c842b34bc6150e91de48d2bafabe7af7f39f2405 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sat, 12 Feb 2022 23:31:06 -0800 Subject: [PATCH 24/31] `BasicOperationQueue.status` now calls `statusHandler()` on main thread --- .../OperationQueue/BasicOperationQueue.swift | 19 +++++++++++++++---- .../AtomicOperationQueue Tests.swift | 2 +- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index b49f5d8..ffd0d1a 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -44,12 +44,21 @@ open class BasicOperationQueue: OperationQueue { // MARK: - Status + @Atomic private var _status: OperationQueueStatus = .idle /// Operation queue status. /// To observe changes to this value, supply a closure to the `statusHandler` property. - @Atomic public internal(set) var status: OperationQueueStatus = .idle { - didSet { - if status != oldValue { - statusHandler?(status, oldValue) + public internal(set) var status: OperationQueueStatus { + get { + _status + } + set { + let oldValue = _status + _status = newValue + + if newValue != oldValue { + DispatchQueue.main.async { + self.statusHandler?(newValue, oldValue) + } } } } @@ -59,6 +68,7 @@ open class BasicOperationQueue: OperationQueue { /// **OTCore:** /// Handler called any time the `status` property changes. + /// Handler is called async on the main thread. public final var statusHandler: StatusHandler? // MARK: - Progress Back-Porting @@ -73,6 +83,7 @@ open class BasicOperationQueue: OperationQueue { /// **OTCore:** /// Set max concurrent operation count. + /// Status handler is called async on the main thread. public init(type operationQueueType: OperationQueueType, resetProgressWhenFinished: Bool = false, statusHandler: StatusHandler? = nil) { diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index cd4265e..9658391 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -85,7 +85,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { opQ.isSuspended = false - wait(for: opQ.status == .idle, timeout: 1.0) + wait(for: opQ.status == .idle, timeout: 2.0) XCTAssertEqual(opQ.status, .idle) XCTAssertEqual(opQ.sharedMutableValue.count, 100) From a57a8acd30e3b46813e9cc38a30fa4af08f49a66 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 13 Feb 2022 00:25:02 -0800 Subject: [PATCH 25/31] `BasicOperationQueue`: Improved KVO handling logic --- .../OperationQueue/BasicOperationQueue.swift | 29 ++++++++++--------- .../AtomicOperationQueue Tests.swift | 3 +- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index ffd0d1a..66bd8ea 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -73,7 +73,7 @@ open class BasicOperationQueue: OperationQueue { // MARK: - Progress Back-Porting - @Atomic private var _progress: Progress = .init() + private var _progress: Progress = .init() @available(macOS 10.9, iOS 7.0, tvOS 9.0, watchOS 2.0, *) @objc dynamic @@ -208,7 +208,7 @@ open class BasicOperationQueue: OperationQueue { message: progress.localizedDescription ) } else { - setStatusIdle() + setStatusIdle(resetProgress: resetProgressWhenFinished) } } } @@ -222,12 +222,14 @@ open class BasicOperationQueue: OperationQueue { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! guard !isSuspended else { return } - guard !progress.isFinished else { return } - if self.operationCount > 0 { + + if !progress.isFinished, + operationCount > 0 + { status = .inProgress(fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription) } else { - setStatusIdle() + setStatusIdle(resetProgress: resetProgressWhenFinished) } } ) @@ -241,11 +243,11 @@ open class BasicOperationQueue: OperationQueue { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! guard !isSuspended else { return } - guard !progress.isFinished, - operationCount > 0 else { - setStatusIdle() - return - } + + if progress.isFinished || operationCount == 0 { + setStatusIdle(resetProgress: resetProgressWhenFinished) + return + } status = .inProgress(fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription) } @@ -260,7 +262,7 @@ open class BasicOperationQueue: OperationQueue { // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! if progress.isFinished { - setStatusIdle() + setStatusIdle(resetProgress: resetProgressWhenFinished) } } ) @@ -275,8 +277,9 @@ open class BasicOperationQueue: OperationQueue { } /// Only call as a result of the queue emptying - private func setStatusIdle() { - if resetProgressWhenFinished { + private func setStatusIdle(resetProgress: Bool) { + if resetProgress, + progress.totalUnitCount != 0 { progress.totalUnitCount = 0 } status = .idle diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index 9658391..7980901 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -58,7 +58,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { } /// Concurrent automatic threading. Do not run it. Check status. Run it. Check status. - func testOp_concurrentAutomatic_NotRun() { + func testOp_concurrentAutomatic_Pause_Run() { let opQ = AtomicOperationQueue(type: .concurrentAutomatic, initialMutableValue: [Int]()) @@ -84,6 +84,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { XCTAssertEqual(opQ.status, .paused) opQ.isSuspended = false + wait(for: (opQ.status != .paused && opQ.status != .idle), timeout: 0.2) wait(for: opQ.status == .idle, timeout: 2.0) XCTAssertEqual(opQ.status, .idle) From 4d4abe2b2fd266841104449fb1e77a2e8db3e1fc Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 13 Feb 2022 01:37:54 -0800 Subject: [PATCH 26/31] `BasicOperationQueue`: Fixed `resetProgressWhenFinished` behavior --- .../BasicOperationQueue Status.swift | 20 +++++ .../OperationQueue/BasicOperationQueue.swift | 38 ++++++-- .../BasicOperationQueue Tests.swift | 88 +++++++++++++++---- 3 files changed, 121 insertions(+), 25 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift index 8e84287..6574ef3 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift @@ -6,6 +6,7 @@ #if canImport(Foundation) import Foundation +import CloudKit /// **OTCore:** /// Operation queue status. @@ -27,4 +28,23 @@ public enum OperationQueueStatus: Equatable, Hashable { } +extension OperationQueueStatus: CustomStringConvertible { + + public var description: String { + + switch self { + case .idle: + return "idle" + + case .inProgress(let fractionCompleted, let message): + return "\(fractionCompleted) \(message.quoted)" + + case .paused: + return "paused" + } + + } + +} + #endif diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 66bd8ea..8775fc0 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -42,6 +42,8 @@ open class BasicOperationQueue: OperationQueue { } + private var done = true + // MARK: - Status @Atomic private var _status: OperationQueueStatus = .idle @@ -127,6 +129,8 @@ open class BasicOperationQueue: OperationQueue { } lastAddedOperation = op + + done = false super.addOperation(op) } @@ -180,6 +184,8 @@ open class BasicOperationQueue: OperationQueue { } lastAddedOperation = ops.last + + done = false super.addOperations(ops, waitUntilFinished: wait) } @@ -202,13 +208,15 @@ open class BasicOperationQueue: OperationQueue { if isSuspended { status = .paused } else { - if operationCount > 0 { + if done { + setStatusIdle(resetProgress: resetProgressWhenFinished) + } else { +if progress.fractionCompleted == 0.0 { print("ZERO from .isSuspended KVO") } status = .inProgress( fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription ) - } else { - setStatusIdle(resetProgress: resetProgressWhenFinished) + } } } @@ -223,9 +231,11 @@ open class BasicOperationQueue: OperationQueue { guard !isSuspended else { return } - if !progress.isFinished, + if !done, + !progress.isFinished, operationCount > 0 { +if progress.fractionCompleted == 0.0 { print("ZERO from .operationCount KVO") } status = .inProgress(fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription) } else { @@ -244,12 +254,17 @@ open class BasicOperationQueue: OperationQueue { guard !isSuspended else { return } - if progress.isFinished || operationCount == 0 { + if done || + progress.isFinished || + progress.completedUnitCount == progress.totalUnitCount || + operationCount == 0 + { setStatusIdle(resetProgress: resetProgressWhenFinished) - return + } else { +if progress.fractionCompleted == 0.0 { print("ZERO from .progress.fractionCompleted KVO. progress.isFinished:", progress.isFinished, "operationCount:", operationCount) } + status = .inProgress(fractionCompleted: progress.fractionCompleted, + message: progress.localizedDescription) } - status = .inProgress(fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription) } ) @@ -279,9 +294,14 @@ open class BasicOperationQueue: OperationQueue { /// Only call as a result of the queue emptying private func setStatusIdle(resetProgress: Bool) { if resetProgress, - progress.totalUnitCount != 0 { + progress.totalUnitCount != 0, + progress.completedUnitCount != 0 + { progress.totalUnitCount = 0 + progress.completedUnitCount = 0 } + + done = true status = .idle } diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index 21c032e..07aa72b 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -78,26 +78,82 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { func testResetProgressWhenFinished_True() { - let opQ = BasicOperationQueue(type: .serialFIFO, - resetProgressWhenFinished: true) - - for _ in 1...10 { - opQ.addOperation { sleep(0.1) } + class OpQProgressTest { + var statuses: [OperationQueueStatus] = [] + + let opQ = BasicOperationQueue(type: .serialFIFO, + resetProgressWhenFinished: true) + + init() { + opQ.statusHandler = { newStatus, oldStatus in + if self.statuses.isEmpty { + self.statuses.append(oldStatus) + print("-", oldStatus) + } + self.statuses.append(newStatus) + print("-", newStatus) + } + } } - XCTAssertEqual(opQ.progress.totalUnitCount, 10) - - switch opQ.status { - case .inProgress(fractionCompleted: _, message: _): - break // correct - default: - XCTFail() + let ppQProgressTest = OpQProgressTest() + + func runTen() { + print("Running 10 operations...") + for _ in 1...10 { + ppQProgressTest.opQ.addOperation { sleep(0.1) } + } + + XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 10) + + switch ppQProgressTest.opQ.status { + case .inProgress(fractionCompleted: _, message: _): + break // correct + default: + XCTFail() + } + + wait(for: ppQProgressTest.opQ.status == .idle, timeout: 1.5) + + wait(for: ppQProgressTest.opQ.progress.totalUnitCount == 0, timeout: 0.5) + XCTAssertEqual(ppQProgressTest.opQ.progress.completedUnitCount, 0) + XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 0) } - wait(for: opQ.status == .idle, timeout: 1.5) - - wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) - XCTAssertEqual(opQ.progress.totalUnitCount, 0) + // run this global async, since the statusHandler gets called on main + let runExp = expectation(description: "Test Run") + DispatchQueue.global().async { + runTen() + runTen() + runExp.fulfill() + } + wait(for: [runExp], timeout: 5.0) + + XCTAssertEqual(ppQProgressTest.statuses, [ + .idle, + .inProgress(fractionCompleted: 0.0, message: "0% completed"), + .inProgress(fractionCompleted: 0.1, message: "10% completed"), + .inProgress(fractionCompleted: 0.2, message: "20% completed"), + .inProgress(fractionCompleted: 0.3, message: "30% completed"), + .inProgress(fractionCompleted: 0.4, message: "40% completed"), + .inProgress(fractionCompleted: 0.5, message: "50% completed"), + .inProgress(fractionCompleted: 0.6, message: "60% completed"), + .inProgress(fractionCompleted: 0.7, message: "70% completed"), + .inProgress(fractionCompleted: 0.8, message: "80% completed"), + .inProgress(fractionCompleted: 0.9, message: "90% completed"), + .idle, + .inProgress(fractionCompleted: 0.0, message: "0% completed"), + .inProgress(fractionCompleted: 0.1, message: "10% completed"), + .inProgress(fractionCompleted: 0.2, message: "20% completed"), + .inProgress(fractionCompleted: 0.3, message: "30% completed"), + .inProgress(fractionCompleted: 0.4, message: "40% completed"), + .inProgress(fractionCompleted: 0.5, message: "50% completed"), + .inProgress(fractionCompleted: 0.6, message: "60% completed"), + .inProgress(fractionCompleted: 0.7, message: "70% completed"), + .inProgress(fractionCompleted: 0.8, message: "80% completed"), + .inProgress(fractionCompleted: 0.9, message: "90% completed"), + .idle + ]) } From 6888ae76656a1519feff95ad9912b68de80f1381 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 13 Feb 2022 02:01:15 -0800 Subject: [PATCH 27/31] `BasicOperationQueue`: Improved child progress reporting to `statusHandler()` --- .../OperationQueue/BasicOperationQueue.swift | 13 ++++++------- .../OperationQueue/AtomicOperationQueue Tests.swift | 2 +- .../OperationQueue/BasicOperationQueue Tests.swift | 4 ++-- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 8775fc0..3552035 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -123,9 +123,10 @@ open class BasicOperationQueue: OperationQueue { // update progress progress.totalUnitCount += 1 if let basicOp = op as? BasicOperation { - // OperationQueue considers each operation to be 1 unit of progress in the overall queue progress, regardless of how the child operation progress decides to set up its total unit count + progress.totalUnitCount += 99 // addOperation() will add 1 more + // give 100 units of progress in case child progress reports fractional progress progress.addChild(basicOp.progress, - withPendingUnitCount: 1) + withPendingUnitCount: 100) } lastAddedOperation = op @@ -177,9 +178,10 @@ open class BasicOperationQueue: OperationQueue { progress.totalUnitCount += Int64(ops.count) for op in ops { if let basicOp = op as? BasicOperation { - // OperationQueue considers each operation to be 1 unit of progress in the overall queue progress, regardless of how the child operation progress decides to set up its total unit count + progress.totalUnitCount += 99 // addOperation() will add 1 more + // give 100 units of progress in case child progress reports fractional progress progress.addChild(basicOp.progress, - withPendingUnitCount: 1) + withPendingUnitCount: 100) } } @@ -211,7 +213,6 @@ open class BasicOperationQueue: OperationQueue { if done { setStatusIdle(resetProgress: resetProgressWhenFinished) } else { -if progress.fractionCompleted == 0.0 { print("ZERO from .isSuspended KVO") } status = .inProgress( fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription @@ -235,7 +236,6 @@ if progress.fractionCompleted == 0.0 { print("ZERO from .isSuspended KVO") } !progress.isFinished, operationCount > 0 { -if progress.fractionCompleted == 0.0 { print("ZERO from .operationCount KVO") } status = .inProgress(fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription) } else { @@ -261,7 +261,6 @@ if progress.fractionCompleted == 0.0 { print("ZERO from .operationCount KVO") } { setStatusIdle(resetProgress: resetProgressWhenFinished) } else { -if progress.fractionCompleted == 0.0 { print("ZERO from .progress.fractionCompleted KVO. progress.isFinished:", progress.isFinished, "operationCount:", operationCount) } status = .inProgress(fractionCompleted: progress.fractionCompleted, message: progress.localizedDescription) } diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift index 7980901..fc403f0 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift @@ -165,7 +165,7 @@ final class Threading_AtomicOperationQueue_Tests: XCTestCase { opQ.addInteractiveOperation { _,_ in sleep(0.1) } } - XCTAssertEqual(opQ.progress.totalUnitCount, 10) + XCTAssertEqual(opQ.progress.totalUnitCount, 10 * 100) switch opQ.status { case .inProgress(fractionCompleted: _, message: _): diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift index 07aa72b..e5b8717 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift @@ -72,7 +72,7 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { wait(for: opQ.status == .idle, timeout: 0.5) wait(for: opQ.operationCount == 0, timeout: 0.5) - XCTAssertEqual(opQ.progress.totalUnitCount, 10) + XCTAssertEqual(opQ.progress.totalUnitCount, 10 * 100) } @@ -104,7 +104,7 @@ final class Threading_BasicOperationQueue_Tests: XCTestCase { ppQProgressTest.opQ.addOperation { sleep(0.1) } } - XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 10) + XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 10 * 100) switch ppQProgressTest.opQ.status { case .inProgress(fractionCompleted: _, message: _): From c02de4322cf608b6a743d998ef1e6fb4626c0fde Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Sun, 13 Feb 2022 03:21:04 -0800 Subject: [PATCH 28/31] `AtomicBlockOperation` now reports progress for all nested children correctly --- .../Complex/AtomicBlockOperation.swift | 31 ++++++- .../OperationQueue/BasicOperationQueue.swift | 11 ++- .../Complex/AtomicBlockOperation Tests.swift | 85 +++++++++++++++++++ 3 files changed, 121 insertions(+), 6 deletions(-) diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift index 01a8e4d..edb47b0 100644 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift @@ -52,32 +52,59 @@ import Foundation /// - note: Inherits from both `BasicAsyncOperation` and `BasicOperation`. open class AtomicBlockOperation: BasicOperation { + // MARK: - Operations + private var operationQueueType: OperationQueueType { + operationQueue.operationQueueType + } private let operationQueue: AtomicOperationQueue! /// **OTCore:** /// Stores a weak reference to the last `Operation` added to the internal operation queue. If the operation is complete and the queue is empty, this may return `nil`. - public weak var lastAddedOperation: Operation? { + public final weak var lastAddedOperation: Operation? { + operationQueue.lastAddedOperation + + } + + public override var progress: Progress { + + operationQueue.progress + } + // MARK: - Shared Mutable Value + /// **OTCore:** /// The thread-safe shared mutable value that all operation blocks operate upon. public final var value: T { + operationQueue.sharedMutableValue + } /// **OTCore:** /// Mutate the shared atomic variable in a closure. - public func mutateValue(_ block: (inout T) -> Void) { + public final func mutateValue(_ block: (inout T) -> Void) { block(&operationQueue.sharedMutableValue) } + // MARK: - Status + + /// **OTCore:** + /// Operation queue status. + /// To observe changes to this value, supply a closure to the `statusHandler` property. + public final var status: OperationQueueStatus { + + operationQueue.status + + } + /// **OTCore:** /// Handler called any time the `status` property changes. public final var statusHandler: BasicOperationQueue.StatusHandler? { diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 3552035..18aa7dd 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -47,6 +47,7 @@ open class BasicOperationQueue: OperationQueue { // MARK: - Status @Atomic private var _status: OperationQueueStatus = .idle + /// **OTCore:** /// Operation queue status. /// To observe changes to this value, supply a closure to the `statusHandler` property. public internal(set) var status: OperationQueueStatus { @@ -121,12 +122,13 @@ open class BasicOperationQueue: OperationQueue { } // update progress - progress.totalUnitCount += 1 if let basicOp = op as? BasicOperation { - progress.totalUnitCount += 99 // addOperation() will add 1 more + progress.totalUnitCount += 100 // give 100 units of progress in case child progress reports fractional progress progress.addChild(basicOp.progress, withPendingUnitCount: 100) + } else { + progress.totalUnitCount += 1 } lastAddedOperation = op @@ -175,13 +177,14 @@ open class BasicOperationQueue: OperationQueue { } // update progress - progress.totalUnitCount += Int64(ops.count) for op in ops { if let basicOp = op as? BasicOperation { - progress.totalUnitCount += 99 // addOperation() will add 1 more + progress.totalUnitCount += 100 // give 100 units of progress in case child progress reports fractional progress progress.addChild(basicOp.progress, withPendingUnitCount: 100) + } else { + progress.totalUnitCount += 1 } } diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift index cab2b69..0801055 100644 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift @@ -399,6 +399,91 @@ final class Threading_AtomicBlockOperation_Tests: XCTestCase { } + /// Ensure that nested progress objects successfully result in the topmost queue calling statusHandler at every increment of all progress children at every level. + func testProgress() { + + class OpQProgressTest { + var statuses: [OperationQueueStatus] = [] + + let mainOp = AtomicOperationQueue(type: .serialFIFO, + qualityOfService: .default, + initiallySuspended: true, + resetProgressWhenFinished: true, + initialMutableValue: 0) + + init() { + mainOp.statusHandler = { newStatus, oldStatus in + if self.statuses.isEmpty { + self.statuses.append(oldStatus) + print("-", oldStatus) + } + self.statuses.append(newStatus) + print("-", newStatus) + } + } + } + + let ppQProgressTest = OpQProgressTest() + + func runTest() { + // 5 ops, each with 2 ops, each with 2 units of progress. + // should equate to 20 total main progress updates 5% apart + for _ in 1...5 { + let subOp = AtomicBlockOperation(type: .serialFIFO, + initialMutableValue: 0) + + for _ in 1...2 { + subOp.addInteractiveOperation { operation, atomicValue in + operation.progress.totalUnitCount = 2 + + operation.progress.completedUnitCount = 1 + operation.progress.completedUnitCount = 2 + } + } + + ppQProgressTest.mainOp.addOperation(subOp) + } + + ppQProgressTest.mainOp.isSuspended = false + + wait(for: ppQProgressTest.mainOp.status == .idle, timeout: 2.0) + } + + let runExp = expectation(description: "Test Run") + DispatchQueue.global().async { + runTest() + runExp.fulfill() + } + wait(for: [runExp], timeout: 5.0) + + XCTAssertEqual(ppQProgressTest.statuses, [ + .idle, + .paused, + .inProgress(fractionCompleted: 0.00, message: "0% completed"), + .inProgress(fractionCompleted: 0.05, message: "5% completed"), + .inProgress(fractionCompleted: 0.10, message: "10% completed"), + .inProgress(fractionCompleted: 0.15, message: "15% completed"), + .inProgress(fractionCompleted: 0.20, message: "20% completed"), + .inProgress(fractionCompleted: 0.25, message: "25% completed"), + .inProgress(fractionCompleted: 0.30, message: "30% completed"), + .inProgress(fractionCompleted: 0.35, message: "35% completed"), + .inProgress(fractionCompleted: 0.40, message: "40% completed"), + .inProgress(fractionCompleted: 0.45, message: "45% completed"), + .inProgress(fractionCompleted: 0.50, message: "50% completed"), + .inProgress(fractionCompleted: 0.55, message: "55% completed"), + .inProgress(fractionCompleted: 0.60, message: "60% completed"), + .inProgress(fractionCompleted: 0.65, message: "65% completed"), + .inProgress(fractionCompleted: 0.70, message: "70% completed"), + .inProgress(fractionCompleted: 0.75, message: "75% completed"), + .inProgress(fractionCompleted: 0.80, message: "80% completed"), + .inProgress(fractionCompleted: 0.85, message: "85% completed"), + .inProgress(fractionCompleted: 0.90, message: "90% completed"), + .inProgress(fractionCompleted: 0.95, message: "95% completed"), + .idle + ]) + + } + } #endif From 221d05392b2ca2073f48b7eeb44cc88475734a31 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 14 Feb 2022 13:06:06 -0800 Subject: [PATCH 29/31] Moved `@Atomic` to new OTAtomics repo, renamed to `@OTAtomicsThreadSafe` --- Package.swift | 9 +- Sources/OTCore/Atomics/Atomic.swift | 101 -------- .../Foundational/BasicOperation.swift | 5 +- .../OperationQueue/AtomicOperationQueue.swift | 3 +- .../OperationQueue/BasicOperationQueue.swift | 3 +- Tests/OTCoreTests/Atomics/Atomics Tests.swift | 244 ------------------ .../Operation/BlockOperation Tests.swift | 5 +- .../OperationQueue Extensions Tests.swift | 7 +- 8 files changed, 19 insertions(+), 358 deletions(-) delete mode 100644 Sources/OTCore/Atomics/Atomic.swift delete mode 100644 Tests/OTCoreTests/Atomics/Atomics Tests.swift diff --git a/Package.swift b/Package.swift index 0588808..6053a0e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,5 +1,4 @@ // swift-tools-version:5.3 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription @@ -22,18 +21,20 @@ let package = Package( ], dependencies: [ + .package(url: "https://github.com/orchetect/OTAtomics", from: "1.0.0"), + // testing-only dependency - .package(url: "https://github.com/orchetect/SegmentedProgress", from: "1.0.1"), + .package(url: "https://github.com/orchetect/SegmentedProgress", from: "1.0.1") ], targets: [ .target( name: "OTCore", - dependencies: []), + dependencies: ["OTAtomics"]), .testTarget( name: "OTCoreTests", - dependencies: ["OTCore", "SegmentedProgress"]) + dependencies: ["OTCore", "OTAtomics", "SegmentedProgress"]) ] ) diff --git a/Sources/OTCore/Atomics/Atomic.swift b/Sources/OTCore/Atomics/Atomic.swift deleted file mode 100644 index b8a53e7..0000000 --- a/Sources/OTCore/Atomics/Atomic.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Atomic.swift -// OTCore • https://github.com/orchetect/OTCore -// - -import Foundation - -/// **OTCore:** -/// Atomic: A property wrapper that ensures thread-safe atomic access to a value. -/// Multiple read accesses can potentially read at the same time, just not during a write. -/// -/// By using `pthread` to do the locking, this safer than using a `DispatchQueue/barrier` as there isn't a chance of priority inversion. -/// -/// This is safe to use on collection types (`Array`, `Dictionary`, etc.) -@propertyWrapper -public final class Atomic { - - private var value: T - - private let lock: ThreadLock = RWThreadLock() - - public init(wrappedValue value: T) { - - self.value = value - - } - - public var wrappedValue: T { - - get { - self.lock.readLock() - defer { self.lock.unlock() } - return self.value - } - - set { - self.lock.writeLock() - value = newValue - self.lock.unlock() - } - - // _modify { } is an internal Swift computed setter, similar to set { } - // however it gives in-place exclusive mutable access - // which allows get-then-set operations such as collection subscripts - // to be performed in a single thread-locked operation - _modify { - self.lock.writeLock() - yield &value - self.lock.unlock() - } - - } - -} - - - - -/// Defines a basic signature to which all locks will conform. Provides the basis for atomic access to stuff. -fileprivate protocol ThreadLock { - - init() - - /// Lock a resource for writing. So only one thing can write, and nothing else can read or write. - func writeLock() - - /// Lock a resource for reading. Other things can also lock for reading at the same time, but nothing else can write at that time. - func readLock() - - /// Unlock a resource - func unlock() - -} - -fileprivate final class RWThreadLock: ThreadLock { - - private var lock = pthread_rwlock_t() - - init() { - guard pthread_rwlock_init(&lock, nil) == 0 else { - preconditionFailure("Unable to initialize the lock") - } - } - - deinit { - pthread_rwlock_destroy(&lock) - } - - func writeLock() { - pthread_rwlock_wrlock(&lock) - } - - func readLock() { - pthread_rwlock_rdlock(&lock) - } - - func unlock() { - pthread_rwlock_unlock(&lock) - } - -} diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift index f5a4465..dc694d9 100644 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift +++ b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift @@ -6,6 +6,7 @@ #if canImport(Foundation) import Foundation +import OTAtomics /// **OTCore:** /// A synchronous or asynchronous `Operation` subclass that provides essential boilerplate. @@ -54,7 +55,7 @@ open class BasicOperation: Operation, ProgressReporting { // adding KVO compliance @objc dynamic public final override var isExecuting: Bool { _isExecuting } - @Atomic private var _isExecuting = false { + @OTAtomicsThreadSafe private var _isExecuting = false { willSet { willChangeValue(for: \.isExecuting) } didSet { didChangeValue(for: \.isExecuting) } } @@ -62,7 +63,7 @@ open class BasicOperation: Operation, ProgressReporting { // adding KVO compliance @objc dynamic public final override var isFinished: Bool { _isFinished } - @Atomic private var _isFinished = false { + @OTAtomicsThreadSafe private var _isFinished = false { willSet { willChangeValue(for: \.isFinished) } didSet { didChangeValue(for: \.isFinished) } } diff --git a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift index 20874fb..fb05185 100644 --- a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift @@ -6,6 +6,7 @@ #if canImport(Foundation) import Foundation +import OTAtomics /// **OTCore:** /// An `OperationQueue` subclass that passes shared thread-safe variable into operation closures. @@ -15,7 +16,7 @@ import Foundation open class AtomicOperationQueue: BasicOperationQueue { /// The thread-safe shared mutable value that all operation blocks operate upon. - @Atomic public final var sharedMutableValue: T + @OTAtomicsThreadSafe public final var sharedMutableValue: T // MARK: - Init diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift index 18aa7dd..f4a8c5e 100644 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift @@ -6,6 +6,7 @@ #if canImport(Foundation) import Foundation +import OTAtomics /// **OTCore:** /// An `OperationQueue` subclass with useful additions. @@ -46,7 +47,7 @@ open class BasicOperationQueue: OperationQueue { // MARK: - Status - @Atomic private var _status: OperationQueueStatus = .idle + @OTAtomicsThreadSafe private var _status: OperationQueueStatus = .idle /// **OTCore:** /// Operation queue status. /// To observe changes to this value, supply a closure to the `statusHandler` property. diff --git a/Tests/OTCoreTests/Atomics/Atomics Tests.swift b/Tests/OTCoreTests/Atomics/Atomics Tests.swift deleted file mode 100644 index 0fb45a6..0000000 --- a/Tests/OTCoreTests/Atomics/Atomics Tests.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// Atomics Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import XCTest -import OTCore - -class Extensions_Swift_Atomics_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - func testAtomic() { - - // baseline read/write functionality test on a variety of types - - class Bar { - var nonAtomicInt: Int = 100 - } - - class Foo { - @Atomic var bool: Bool = true - @Atomic var int: Int = 5 - @Atomic var string: String = "a string" - @Atomic var dict: [String: Int] = ["Key" : 1] - @Atomic var array: [String] = ["A", "B", "C"] - @Atomic var barClass = Bar() - } - - let foo = Foo() - - // read value - - XCTAssertEqual(foo.bool, true) - XCTAssertEqual(foo.int, 5) - XCTAssertEqual(foo.string, "a string") - XCTAssertEqual(foo.dict, ["Key" : 1]) - XCTAssertEqual(foo.array, ["A", "B", "C"]) - XCTAssertEqual(foo.barClass.nonAtomicInt, 100) - - // replace value - - foo.bool = false - XCTAssertEqual(foo.bool, false) - - foo.int = 10 - XCTAssertEqual(foo.int, 10) - - foo.string = "a new string" - XCTAssertEqual(foo.string, "a new string") - - foo.dict = ["KeyA" : 10, "KeyB" : 20] - XCTAssertEqual(foo.dict, ["KeyA" : 10, "KeyB" : 20]) - - foo.array = ["1", "2"] - XCTAssertEqual(foo.array, ["1", "2"]) - - foo.barClass.nonAtomicInt = 50 - XCTAssertEqual(foo.barClass.nonAtomicInt, 50) - - // mutate value (collections) - - foo.dict["KeyB"] = 30 - XCTAssertEqual(foo.dict, ["KeyA" : 10, "KeyB" : 30]) - - foo.array[1] = "3" - XCTAssertEqual(foo.array, ["1", "3"]) - - } - - func testAtomic_BruteForce_ConcurrentMutations() { - - let completionTimeout = expectation(description: "Test Completion Timeout") - - class Foo { - @Atomic var dict: [String: Int] = [:] - @Atomic var array: [String] = [] - } - - let foo = Foo() - - let g = DispatchGroup() - - let iterations = 10_000 - - // append operations - - for index in 0.. 0 { - let dictIndex = Int.random(in: 0.. 0 { - let arrayIndex = Int.random(in: 0.. 0 { _ = foo.array[0] } - readGroup.leave() - } - } - - DispatchQueue.global().async { - writeGroup.wait() - readGroup.wait() - completionTimeout.fulfill() - } - - wait(for: [completionTimeout], timeout: 10) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift index f1dad5e..4e76b4e 100644 --- a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift +++ b/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift @@ -5,12 +5,13 @@ #if shouldTestCurrentPlatform -@testable import OTCore import XCTest +@testable import OTCore +import OTAtomics final class BlockOperation_Tests: XCTestCase { - @Atomic fileprivate var arr: [Int] = [] + @OTAtomicsThreadSafe fileprivate var arr: [Int] = [] /// This does not test a feature of OTCore. Rather, it tests the behavior of Foundation's built-in `BlockOperation` object. func testBlockOperation() { diff --git a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift index 90a6606..33c974b 100644 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift @@ -5,15 +5,16 @@ #if shouldTestCurrentPlatform -import OTCore import XCTest +import OTCore +import OTAtomics final class Threading_OperationQueueExtensions_Success_Tests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } - @Atomic fileprivate var val = 0 + @OTAtomicsThreadSafe fileprivate var val = 0 func testWaitUntilAllOperationsAreFinished_Timeout_Success() { @@ -44,7 +45,7 @@ final class Threading_OperationQueueExtensions_TimedOut_Tests: XCTestCase { override func setUp() { super.setUp() } override func tearDown() { super.tearDown() } - @Atomic fileprivate var val = 0 + @OTAtomicsThreadSafe fileprivate var val = 0 func testWaitUntilAllOperationsAreFinished_Timeout_TimedOut() { From d833de230697657f8704cb1c3cafb64db7cc3113 Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 14 Feb 2022 14:34:00 -0800 Subject: [PATCH 30/31] Removed `Operation` and `OperationQueue` subclasses, moved to new OTOperations repo --- .../AtomicVariableAccess.swift | 33 -- .../Closure/AsyncClosureOperation.swift | 90 ---- .../Operation/Closure/ClosureOperation.swift | 64 --- .../InteractiveAsyncClosureOperation.swift | 102 ---- .../Closure/InteractiveClosureOperation.swift | 75 --- .../Complex/AtomicBlockOperation.swift | 372 ------------- .../Foundational/BasicAsyncOperation.swift | 56 -- .../Foundational/BasicOperation.swift | 141 ----- .../Operation/Operation Extensions.swift | 59 --- .../OperationQueue/AtomicOperationQueue.swift | 146 ------ .../BasicOperationQueue Status.swift | 50 -- .../OperationQueue/BasicOperationQueue.swift | 320 ------------ .../OperationQueue Extensions.swift | 31 -- .../OperationQueue/OperationQueueType.swift | 25 - .../Operation/BlockOperation Tests.swift | 50 -- .../Closure/AsyncClosureOperation Tests.swift | 245 --------- .../Closure/ClosureOperation Tests.swift | 220 -------- ...teractiveAsyncClosureOperation Tests.swift | 242 --------- .../InteractiveClosureOperation Tests.swift | 145 ------ .../Complex/AtomicBlockOperation Tests.swift | 489 ------------------ .../BasicAsyncOperation Tests.swift | 252 --------- .../Foundational/BasicOperation Tests.swift | 232 --------- .../AtomicOperationQueue Tests.swift | 186 ------- .../BasicOperationQueue Tests.swift | 203 -------- .../OperationQueue Extensions Tests.swift | 74 --- 25 files changed, 3902 deletions(-) delete mode 100644 Sources/OTCore/Threading/Operation Common/AtomicVariableAccess.swift delete mode 100644 Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift delete mode 100644 Sources/OTCore/Threading/Operation/Operation Extensions.swift delete mode 100644 Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift delete mode 100644 Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift delete mode 100644 Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift delete mode 100644 Sources/OTCore/Threading/OperationQueue/OperationQueue Extensions.swift delete mode 100644 Sources/OTCore/Threading/OperationQueue/OperationQueueType.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift delete mode 100644 Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift diff --git a/Sources/OTCore/Threading/Operation Common/AtomicVariableAccess.swift b/Sources/OTCore/Threading/Operation Common/AtomicVariableAccess.swift deleted file mode 100644 index 1da2d1b..0000000 --- a/Sources/OTCore/Threading/Operation Common/AtomicVariableAccess.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// AtomicVariableAccess.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// Proxy object providing mutation access to an atomic variable. -public class AtomicVariableAccess { - - weak private var operationQueue: AtomicOperationQueue? - - internal init(operationQueue: AtomicOperationQueue) { - - self.operationQueue = operationQueue - - } - - /// Mutate the atomic variable in a closure. - /// Warning: Perform as little logic as possible and only use this closure to get or set the variable. Failure to do so may result in deadlocks in complex multi-threaded applications. - public func mutate(_ block: (_ value: inout T) -> Void) { - - guard let operationQueue = operationQueue else { return } - block(&operationQueue.sharedMutableValue) - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift deleted file mode 100644 index 4d381d7..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// AsyncClosureOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// An asynchronous `Operation` subclass that provides essential boilerplate for building an operation and supplies a closure as a convenience when further subclassing is not necessary. -/// -/// This operation is asynchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread and may return control before the operation is complete. -/// -/// **Usage** -/// -/// No special method calls are required in the main block. -/// -/// This closure is not cancellable once it is started, and does not offer a reference to update progress information. If you want to allow cancellation (early return partway through operation execution) or progress updating, use `InteractiveAsyncClosureOperation` instead. -/// -/// // if not specifying a dispatch queue, the operation will -/// // run on the current thread if started manually, -/// // or if this operation is added to an OperationQueue it -/// // will be automatically managed -/// let op = AsyncClosureOperation { -/// // ... do some work ... -/// -/// // operation completes & cleans up automatically -/// // after closure finishes -/// } -/// -/// Execution on a target thread: -/// -/// // force the operation to execute on a dispatch queue, -/// // which may be desirable especially when running -/// // the operation without adding it to an OperationQueue -/// // and the closure body does not contain any asynchronous code -/// let op = AsyncClosureOperation(on: .global()) { -/// -/// } -/// -/// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. -/// -/// // if inserting into an OperationQueue: -/// let opQueue = OperationQueue() -/// opQueue.addOperation(op) -/// -/// // if not inserting into an OperationQueue: -/// op.start() -/// -/// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. -/// -/// - note: Inherits from `BasicOperation`. -public final class AsyncClosureOperation: BasicOperation { - - public final override var isAsynchronous: Bool { true } - - public final let queue: DispatchQueue? - public final let mainBlock: () -> Void - - public init( - on queue: DispatchQueue? = nil, - _ mainBlock: @escaping () -> Void - ) { - - self.queue = queue - self.mainBlock = mainBlock - - } - - override public final func main() { - - guard mainShouldStart() else { return } - - if let queue = queue { - queue.async { [weak self] in - guard let self = self else { return } - self.mainBlock() - self.completeOperation() - } - } else { - mainBlock() - completeOperation() - } - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift deleted file mode 100644 index 18b1ef7..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ClosureOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// A synchronous `Operation` subclass that provides essential boilerplate for building an operation and supplies a closure as a convenience when further subclassing is not necessary. -/// -/// This operation is synchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread. By the time the `start()` method returns control, the operation is complete. -/// -/// **Usage** -/// -/// No special method calls are required in the main block. -/// -/// This closure is not cancellable once it is started, and does not offer a reference to update progress information. If you want to allow cancellation (early return partway through operation execution) or progress updating, use `InteractiveClosureOperation` instead. -/// -/// let op = ClosureOperation { -/// // ... do some work ... -/// -/// // operation completes & cleans up automatically -/// // after closure finishes -/// } -/// -/// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. -/// -/// // if inserting into an OperationQueue: -/// let opQueue = OperationQueue() -/// opQueue.addOperation(op) -/// -/// // if not inserting into an OperationQueue: -/// op.start() -/// -/// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. -/// -/// - note: Inherits from `BasicOperation`. -public final class ClosureOperation: BasicOperation { - - public final override var isAsynchronous: Bool { false } - - public final var mainBlock: () -> Void - - public init( - _ mainBlock: @escaping () -> Void - ) { - - self.mainBlock = mainBlock - - } - - override public func main() { - - guard mainShouldStart() else { return } - mainBlock() - completeOperation() - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift deleted file mode 100644 index cb4144b..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/InteractiveAsyncClosureOperation.swift +++ /dev/null @@ -1,102 +0,0 @@ -// -// InteractiveAsyncClosureOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// An asynchronous `Operation` subclass that provides essential boilerplate and supplies a closure as a convenience when further subclassing is not necessary. -/// -/// This operation is asynchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread and may return control before the operation is complete. -/// -/// **Usage** -/// -/// There is no need to guard `mainShouldStart()` at the start of the block, as the initial check is done for you internally. -/// -/// If progress information is available, set `operation.progress.totalUnitCount` and periodically update `operation.progress.completedUnitCount` through the operation. Cleanup will automatically finish the progress and set it to 100% once the block finishes. -/// -/// It is still best practise to periodically guard `mainShouldAbort()` if the operation may take more than a few seconds. -/// -/// Finally, you must call `completeOperation()` within the closure block once the async operation is fully finished its execution. -/// -/// let op = InteractiveAsyncClosureOperation { operation in -/// // optionally: set progress info -/// operation.progress.totalUnitCount = 100 -/// -/// // ... do some work ... -/// -/// // optionally: update progress periodically -/// operation.progress.completedUnitCount = 50 -/// -/// // optionally: if the operation takes more -/// // than a few seconds on average, -/// // it's good practise to periodically -/// // check if operation is cancelled and return -/// if operation.mainShouldAbort() { return } -/// -/// // ... do some work ... -/// -/// // finally call complete (also sets progress to 100%) -/// operation.completeOperation() -/// } -/// -/// Execution on a target thread: -/// -/// // force the operation to execute on a dispatch queue, -/// // which may be desirable especially when running -/// // the operation without adding it to an OperationQueue -/// // and the closure body does not contain any asynchronous code -/// let op = InteractiveAsyncClosureOperation(on: .global()) { operation in -/// -/// } -/// -/// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. -/// -/// // if inserting into an OperationQueue: -/// let opQueue = OperationQueue() -/// opQueue.addOperation(op) -/// -/// // if not inserting into an OperationQueue: -/// op.start() -/// -/// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. -/// -/// - note: Inherits from both `BasicAsyncOperation` and `BasicOperation`. -public final class InteractiveAsyncClosureOperation: BasicAsyncOperation { - - public final let queue: DispatchQueue? - public final let mainBlock: (_ operation: InteractiveAsyncClosureOperation) -> Void - - public init( - on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: InteractiveAsyncClosureOperation) -> Void - ) { - - self.queue = queue - self.mainBlock = mainBlock - - } - - override public final func main() { - - guard mainShouldStart() else { return } - - if let queue = queue { - queue.async { [weak self] in - guard let self = self else { return } - self.mainBlock(self) - } - } else { - mainBlock(self) - } - - // completeOperation() must be called manually in the block since the block runs async - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift deleted file mode 100644 index 934f7f9..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/InteractiveClosureOperation.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// InteractiveClosureOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// A synchronous `Operation` subclass that provides essential boilerplate for building an operation and supplies a closure as a convenience when further subclassing is not necessary. -/// -/// This operation is synchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread. By the time the `start()` method returns control, the operation is complete. -/// -/// **Usage** -/// -/// No specific calls are required to be made within the main block, however it is best practise to periodically check if the operation is cancelled and return early if the operation may take more than a few seconds. -/// -/// If progress information is available, set `operation.progress.totalUnitCount` and periodically update `operation.progress.completedUnitCount` through the operation. Cleanup will automatically finish the progress and set it to 100% once the block finishes. -/// -/// let op = InteractiveClosureOperation { operation in -/// // optionally: set progress info -/// operation.progress.totalUnitCount = 100 -/// -/// // ... do some work ... -/// -/// // optionally: update progress periodically -/// operation.progress.completedUnitCount = 50 -/// -/// // optionally: if the operation takes more -/// // than a few seconds on average, -/// // it's good practise to periodically -/// // check if operation is cancelled and return -/// if operation.mainShouldAbort() { return } -/// -/// // ... do some work ... -/// } -/// -/// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. -/// -/// // if inserting into an OperationQueue: -/// let opQueue = OperationQueue() -/// opQueue.addOperation(op) -/// -/// // if not inserting into an OperationQueue: -/// op.start() -/// -/// - important: This object is not intended to be subclassed. Rather, it is a simple convenience wrapper when a closure is needed to be wrapped in an `Operation` for when you require a reference to the operation which would not otherwise be available if `.addOperation{}` was called directly on an `OperationQueue`. -/// -/// - note: Inherits from `BasicOperation`. -public final class InteractiveClosureOperation: BasicOperation { - - public final override var isAsynchronous: Bool { false } - - public final var mainBlock: (_ operation: InteractiveClosureOperation) -> Void - - public init( - _ mainBlock: @escaping (_ operation: InteractiveClosureOperation) -> Void - ) { - - self.mainBlock = mainBlock - - } - - override public func main() { - - guard mainShouldStart() else { return } - mainBlock(self) - completeOperation() - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift b/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift deleted file mode 100644 index edb47b0..0000000 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ /dev/null @@ -1,372 +0,0 @@ -// -// AtomicBlockOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// A synchronous `Operation` subclass that is similar to `BlockOperation` but whose internal queue can be serial or concurrent and where sub-operations can reduce upon a shared thread-safe variable passed into the operation closures. -/// -/// **Setup** -/// -/// Instantiate `AtomicBlockOperation` with queue type and initial mutable value. This value can be of any concrete type. If a shared mutable value is not required, an arbitrary value can be passed as the initial value such as 0. -/// -/// Any initial setup necessary can be done using `setSetupBlock{}`. Do not override `main()` or `start()`. -/// -/// For completion, use `.setCompletionBlock{}`. Do not modify the underlying `.completionBlock` directly. -/// -/// let op = AtomicBlockOperation(.serialFIFO, -/// initialMutableValue: 2) -/// op.setSetupBlock { operation, atomicValue in -/// // do some setup -/// } -/// op.addOperation { atomicValue in -/// atomicValue.mutate { $0 += 1 } -/// } -/// op.addOperation { atomicValue in -/// atomicValue.mutate { $0 += 1 } -/// } -/// op.addInteractiveOperation { operation, atomicValue in -/// atomicValue.mutate { $0 += 1 } -/// if operation.mainShouldAbort() { return } -/// atomicValue.mutate { $0 += 1 } -/// } -/// op.setCompletionBlock { atomicValue in -/// print(atomicValue) // "6" -/// } -/// -/// Add the operation to an `OperationQueue` or start it manually if not being inserted into an OperationQueue. -/// -/// // if inserting into an OperationQueue: -/// let opQueue = OperationQueue() -/// opQueue.addOperation(op) -/// -/// // if not inserting into an OperationQueue: -/// op.start() -/// -/// - important: In most use cases, this object does not need to be subclassed. -/// -/// - note: Inherits from both `BasicAsyncOperation` and `BasicOperation`. -open class AtomicBlockOperation: BasicOperation { - - // MARK: - Operations - - private var operationQueueType: OperationQueueType { - - operationQueue.operationQueueType - - } - - private let operationQueue: AtomicOperationQueue! - - /// **OTCore:** - /// Stores a weak reference to the last `Operation` added to the internal operation queue. If the operation is complete and the queue is empty, this may return `nil`. - public final weak var lastAddedOperation: Operation? { - - operationQueue.lastAddedOperation - - } - - public override var progress: Progress { - - operationQueue.progress - - } - - // MARK: - Shared Mutable Value - - /// **OTCore:** - /// The thread-safe shared mutable value that all operation blocks operate upon. - public final var value: T { - - operationQueue.sharedMutableValue - - } - - /// **OTCore:** - /// Mutate the shared atomic variable in a closure. - public final func mutateValue(_ block: (inout T) -> Void) { - - block(&operationQueue.sharedMutableValue) - - } - - // MARK: - Status - - /// **OTCore:** - /// Operation queue status. - /// To observe changes to this value, supply a closure to the `statusHandler` property. - public final var status: OperationQueueStatus { - - operationQueue.status - - } - - /// **OTCore:** - /// Handler called any time the `status` property changes. - public final var statusHandler: BasicOperationQueue.StatusHandler? { - get { - operationQueue.statusHandler - } - set { - operationQueue.statusHandler = newValue - } - } - - private var setupBlock: ((_ operation: AtomicBlockOperation, - _ atomicValue: AtomicVariableAccess) -> Void)? - - // MARK: - Init - - public init(type operationQueueType: OperationQueueType, - initialMutableValue: T, - qualityOfService: QualityOfService? = nil, - resetProgressWhenFinished: Bool = false, - statusHandler: BasicOperationQueue.StatusHandler? = nil) { - - // assign properties - operationQueue = AtomicOperationQueue( - type: operationQueueType, - qualityOfService: qualityOfService, - initiallySuspended: true, - resetProgressWhenFinished: resetProgressWhenFinished, - initialMutableValue: initialMutableValue, - statusHandler: statusHandler - ) - - // super - super.init() - - if let qualityOfService = qualityOfService { - self.qualityOfService = qualityOfService - self.operationQueue.qualityOfService = qualityOfService - } - - // set up observers - addObservers() - - } - - // MARK: - Overrides - - public final override func main() { - - guard mainShouldStart() else { return } - let varAccess = AtomicVariableAccess(operationQueue: self.operationQueue) - setupBlock?(self, varAccess) - - guard operationQueue.operationCount > 0 else { - completeOperation() - return - } - - operationQueue.isSuspended = false - - // this ensures that the operation runs synchronously - // which mirrors the behavior of BlockOperation - while !isFinished { - sleep(0.010) // 10ms - - //Thread.sleep(forTimeInterval: 0.010) - - //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) - } - - } - - // MARK: - KVO Observers - - /// **OTCore:** - /// Retain property observers. For safety, this array must be emptied on class deinit. - private var observers: [NSKeyValueObservation] = [] - private func addObservers() { - - // self.isCancelled - - observers.append( - observe(\.isCancelled, options: [.new]) - { [self, operationQueue] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - if isCancelled { - operationQueue?.cancelAllOperations() - completeOperation(dueToCancellation: true) - } - } - ) - - // self.qualityOfService - - observers.append( - observe(\.qualityOfService, options: [.new]) - { [self, operationQueue] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - // for some reason, change.newValue is nil here. so just read from the property directly. - // guard let newValue = change.newValue else { return } - - // propagate to operation queue - operationQueue?.qualityOfService = qualityOfService - } - ) - - // self.operationQueue.operationCount - - observers.append( - operationQueue.observe(\.operationCount, options: [.new]) - { [self, operationQueue] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - if operationQueue?.operationCount == 0 { - completeOperation() - } - } - ) - - // self.operationQueue.progress.isFinished - // (NSProgress docs state that isFinished is KVO-observable) - - observers.append( - operationQueue.progress.observe(\.isFinished, options: [.new]) - { [self, operationQueue] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - if operationQueue?.progress.isFinished == true { - completeOperation() - } - } - ) - - } - - private func removeObservers() { - - observers.forEach { $0.invalidate() } // for extra safety, invalidate them first - observers.removeAll() - } - - deinit { - - setupBlock = nil - - // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time - removeObservers() - - } - -} - -// MARK: - Proxy methods - -extension AtomicBlockOperation { - - /// **OTCore:** - /// Add an operation block operating on the shared mutable value. - /// - /// - returns: The new operation. - @discardableResult - public final func addOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) -> ClosureOperation { - - operationQueue.addOperation(dependencies: dependencies, block) - - } - - /// **OTCore:** - /// Add an operation block operating on the shared mutable value. - /// `operation.mainShouldAbort()` can be periodically called and then early return if the operation may take more than a few seconds. - /// - /// - returns: The new operation. - @discardableResult - public final func addInteractiveOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ operation: InteractiveClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> InteractiveClosureOperation { - - operationQueue.addInteractiveOperation(dependencies: dependencies, block) - - } - - /// **OTCore:** - /// Add an operation to the operation queue. - public final func addOperation(_ op: Operation){ - - operationQueue.addOperation(op) - - } - - /// **OTCore:** - /// Add operations to the operation queue. - public final func addOperations(_ ops: [Operation], - waitUntilFinished: Bool) { - - operationQueue.addOperations(ops, - waitUntilFinished: waitUntilFinished) - - } - - /// **OTCore:** - /// Add a barrier block operation to the operation queue. - /// - /// Invoked after all currently enqueued operations have finished. Operations you add after the barrier block don’t start until the block has completed. - @available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) - public final func addBarrierBlock( - _ barrier: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) { - - operationQueue.addBarrierBlock(barrier) - - } - - /// **OTCore:** - /// Blocks the current thread until all the receiver’s queued and executing operations finish executing. - public func waitUntilAllOperationsAreFinished(timeout: DispatchTimeInterval? = nil) { - - if let timeout = timeout { - operationQueue.waitUntilAllOperationsAreFinished(timeout: timeout) - } else { - operationQueue.waitUntilAllOperationsAreFinished() - } - - } - -} - -// MARK: - Blocks - -extension AtomicBlockOperation { - - /// **OTCore:** - /// Add a setup block that runs when the `AtomicBlockOperation` starts. - public final func setSetupBlock( - _ block: @escaping (_ operation: AtomicBlockOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) { - - setupBlock = block - - } - - /// **OTCore:** - /// Add a completion block that runs when the `AtomicBlockOperation` completes all its operations. - public final func setCompletionBlock( - _ block: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) { - - completionBlock = { [weak self] in - guard let self = self else { return } - let varAccess = AtomicVariableAccess(operationQueue: self.operationQueue) - block(varAccess) - } - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift deleted file mode 100644 index 7f5f4ab..0000000 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// BasicAsyncOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// An asynchronous `Operation` subclass that provides essential boilerplate. -/// `BasicAsyncOperation` is designed to be subclassed. -/// -/// **Important Information from Apple Docs** -/// -/// If you always plan to use queues to execute your operations, it is simpler to define them as synchronous (by subclassing OTCore's `BasicOperation` instead). If you execute operations manually, though, you might want to define your operation objects as asynchronous. Defining an asynchronous operation requires more work, because you have to monitor the ongoing state of your task and report changes in that state using KVO notifications. But defining asynchronous operations is useful in cases where you want to ensure that a manually executed operation does not block the calling thread. -/// -/// When you call the `start()` method of an asynchronous operation, that method may return before the corresponding task is completed. An asynchronous operation object is responsible for scheduling its task on a separate thread. The operation could do that by starting a new thread directly, by calling an asynchronous method, or by submitting a block to a dispatch queue for execution. It does not actually matter if the operation is ongoing when control returns to the caller, only that it could be ongoing. -/// -/// When you add an operation to an operation queue, the queue ignores the value of the `isAsynchronous` property and always calls the `start()` method from a separate thread. Therefore, if you always run operations by adding them to an operation queue, there is no reason to make them asynchronous. -/// -/// **Usage** -/// -/// This object is designed to be subclassed. -/// -/// Refer to the following example for calls that must be made within the main closure block: -/// -/// class MyOperation: BasicAsyncOperation { -/// override func main() { -/// // At the start, call this and conditionally return: -/// guard mainShouldStart() else { return } -/// -/// // ... do some work ... -/// -/// // Optionally: -/// // If the operation may take more than a few seconds, -/// // periodically check and and return early: -/// if mainShouldAbort() { return } -/// -/// // ... do some work ... -/// -/// // Finally, at the end of the operation call: -/// completeOperation() -/// } -/// } -/// -/// - note: This object is designed to be subclassed. See the Foundation documentation for `Operation` regarding overriding `start()` and be sure to follow the guidelines in these inline docs regarding `BasicAsyncOperation` specifically. -/// -/// - note: Inherits from `BasicOperation`. -open class BasicAsyncOperation: BasicOperation { - - final public override var isAsynchronous: Bool { true } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift b/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift deleted file mode 100644 index dc694d9..0000000 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// BasicOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation -import OTAtomics - -/// **OTCore:** -/// A synchronous or asynchronous `Operation` subclass that provides essential boilerplate. -/// `BasicOperation` is designed to be subclassed. -/// -/// By default this operation is synchronous. If the operation is run without being inserted into an `OperationQueue`, when you call the `start()` method the operation executes immediately in the current thread. By the time the `start()` method returns control, the operation is complete. -/// -/// If asynchronous behavior is required then use `BasicAsyncOperation` instead. -/// -/// **Usage** -/// -/// This object is designed to be subclassed. -/// -/// Refer to the following example for calls that must be made within the main closure block: -/// -/// class MyOperation: BasicOperation { -/// override func main() { -/// // At the start, call this and conditionally return: -/// guard mainShouldStart() else { return } -/// -/// // ... do some work ... -/// -/// // Optionally: -/// // If the operation may take more than a few seconds, -/// // periodically check and and return early: -/// if mainShouldAbort() { return } -/// -/// // ... do some work ... -/// -/// // Finally, at the end of the operation call: -/// completeOperation() -/// } -/// } -/// -/// - important: This object is designed to be subclassed. See the Foundation documentation for `Operation` regarding overriding `start()` and be sure to follow the guidelines in these inline docs regarding `BasicOperation` specifically. -open class BasicOperation: Operation, ProgressReporting { - - // MARK: - Progress - - /// **OTCore:** - /// Progress object representing progress of the operation. - public private(set) var progress: Progress = .init(totalUnitCount: 1) - - // MARK: - KVO - - // adding KVO compliance - @objc dynamic - public final override var isExecuting: Bool { _isExecuting } - @OTAtomicsThreadSafe private var _isExecuting = false { - willSet { willChangeValue(for: \.isExecuting) } - didSet { didChangeValue(for: \.isExecuting) } - } - - // adding KVO compliance - @objc dynamic - public final override var isFinished: Bool { _isFinished } - @OTAtomicsThreadSafe private var _isFinished = false { - willSet { willChangeValue(for: \.isFinished) } - didSet { didChangeValue(for: \.isFinished) } - } - - // adding KVO compliance - @objc dynamic - public final override var qualityOfService: QualityOfService { - get { _qualityOfService } - set { _qualityOfService = newValue } - } - private var _qualityOfService: QualityOfService = .default { - willSet { willChangeValue(for: \.qualityOfService) } - didSet { didChangeValue(for: \.qualityOfService) } - } - - // MARK: - Method Overrides - - public final override func start() { - if isCancelled { completeOperation(dueToCancellation: true) } - super.start() - } - - public final override func cancel() { - super.cancel() - progress.cancel() - } - - // MARK: - Methods - - /// **OTCore:** - /// Returns true if operation should begin. - public final func mainShouldStart() -> Bool { - - guard !isCancelled else { - completeOperation(dueToCancellation: true) - return false - } - - guard !isExecuting else { return false } - _isExecuting = true - return true - - } - - /// **OTCore:** - /// Call this once all execution is complete in the operation. - /// If returning early from the operation due to `isCancelled` being true, call this with the `dueToCancellation` flag set to `true` to update this operation's progress as cancelled. - public final func completeOperation(dueToCancellation: Bool = false) { - - if isCancelled || dueToCancellation { - progress.cancel() - } else { - progress.completedUnitCount = progress.totalUnitCount - } - - _isExecuting = false - _isFinished = true - - } - - /// **OTCore:** - /// Checks if `isCancelled` is true, and calls `completedOperation()` if so. - /// Returns `isCancelled`. - public final func mainShouldAbort() -> Bool { - - if isCancelled { - completeOperation(dueToCancellation: true) - } - return isCancelled - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/Operation/Operation Extensions.swift b/Sources/OTCore/Threading/Operation/Operation Extensions.swift deleted file mode 100644 index ed96b73..0000000 --- a/Sources/OTCore/Threading/Operation/Operation Extensions.swift +++ /dev/null @@ -1,59 +0,0 @@ -// -// Operation Extensions.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -extension Operation { - - /// **OTCore:** - /// Convenience static constructor for `ClosureOperation`. - public static func basic( - _ mainBlock: @escaping () -> Void - ) -> ClosureOperation { - - .init(mainBlock) - - } - - /// **OTCore:** - /// Convenience static constructor for `InteractiveClosureOperation`. - public static func interactive( - _ mainBlock: @escaping (_ operation: InteractiveClosureOperation) -> Void - ) -> InteractiveClosureOperation { - - .init(mainBlock) - - } - - /// **OTCore:** - /// Convenience static constructor for `InteractiveAsyncClosureOperation`. - public static func interactiveAsync( - on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: InteractiveAsyncClosureOperation) -> Void - ) -> InteractiveAsyncClosureOperation { - - .init(on: queue, - mainBlock) - - } - - /// **OTCore:** - /// Convenience static constructor for `AtomicBlockOperation`. - /// Builder pattern can be used to add operations inline. - public static func atomicBlock( - _ operationQueueType: OperationQueueType, - initialMutableValue: T - ) -> AtomicBlockOperation { - - .init(type: operationQueueType, - initialMutableValue: initialMutableValue) - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift deleted file mode 100644 index fb05185..0000000 --- a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// AtomicOperationQueue.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation -import OTAtomics - -/// **OTCore:** -/// An `OperationQueue` subclass that passes shared thread-safe variable into operation closures. -/// Concurrency type can be specified. -/// -/// - note: Inherits from `BasicOperationQueue`. -open class AtomicOperationQueue: BasicOperationQueue { - - /// The thread-safe shared mutable value that all operation blocks operate upon. - @OTAtomicsThreadSafe public final var sharedMutableValue: T - - // MARK: - Init - - public init( - type operationQueueType: OperationQueueType = .concurrentAutomatic, - qualityOfService: QualityOfService? = nil, - initiallySuspended: Bool = false, - resetProgressWhenFinished: Bool = false, - initialMutableValue: T, - statusHandler: BasicOperationQueue.StatusHandler? = nil - ) { - - self.sharedMutableValue = initialMutableValue - - super.init(type: operationQueueType, - resetProgressWhenFinished: resetProgressWhenFinished, - statusHandler: statusHandler) - - if let qualityOfService = qualityOfService { - self.qualityOfService = qualityOfService - } - - if initiallySuspended { - isSuspended = true - } - - } - - // MARK: - Shared Mutable Value Methods - /// **OTCore:** - /// Add an operation block operating on the shared mutable value. - /// - /// - returns: The new operation. - @discardableResult - public final func addOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) -> ClosureOperation { - - let op = createOperation(block) - dependencies.forEach { op.addDependency($0) } - addOperation(op) - return op - - } - - /// **OTCore:** - /// Add an operation block operating on the shared mutable value. - /// `operation.mainShouldAbort()` can be periodically called and then early return if the operation may take more than a few seconds. - /// - /// - returns: The new operation. - @discardableResult - public final func addInteractiveOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ operation: InteractiveClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> InteractiveClosureOperation { - - let op = createInteractiveOperation(block) - dependencies.forEach { op.addDependency($0) } - addOperation(op) - return op - - } - - /// **OTCore:** - /// Add a barrier block operation to the operation queue. - /// - /// Invoked after all currently enqueued operations have finished. Operations you add after the barrier block don’t start until the block has completed. - @available(macOS 10.15, iOS 13.0, tvOS 13, watchOS 6, *) - public final func addBarrierBlock( - _ barrier: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) { - - addBarrierBlock { [weak self] in - guard let self = self else { return } - let varAccess = AtomicVariableAccess(operationQueue: self) - barrier(varAccess) - } - - } - - // MARK: - Factory Methods - - /// **OTCore:** - /// Internal for debugging: - /// Create an operation block operating on the shared mutable value. - internal final func createOperation( - _ block: @escaping (_ atomicValue: AtomicVariableAccess) -> Void - ) -> ClosureOperation { - - ClosureOperation { [weak self] in - guard let self = self else { return } - let varAccess = AtomicVariableAccess(operationQueue: self) - block(varAccess) - } - - } - - /// **OTCore:** - /// Internal for debugging: - /// Create an operation block operating on the shared mutable value. - /// `operation.mainShouldAbort()` can be periodically called and then early return if the operation may take more than a few seconds. - internal final func createInteractiveOperation( - _ block: @escaping (_ operation: InteractiveClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> InteractiveClosureOperation { - - InteractiveClosureOperation { [weak self] operation in - guard let self = self else { return } - let varAccess = AtomicVariableAccess(operationQueue: self) - block(operation, varAccess) - } - - } - - /// **OTCore:** - /// Mutate the shared atomic variable in a closure. - public func mutateValue(_ block: (inout T) -> Void) { - - block(&sharedMutableValue) - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift deleted file mode 100644 index 6574ef3..0000000 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue Status.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// BasicOperationQueue Status.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation -import CloudKit - -/// **OTCore:** -/// Operation queue status. -/// Used by `BasicOperationQueue` and its subclasses. -public enum OperationQueueStatus: Equatable, Hashable { - - /// Operation queue is empty. No operations are executing. - case idle - - /// Operation queue is executing one or more operations. - /// - Parameters: - /// - fractionCompleted: progress between 0.0...1.0 - /// - message: displayable string describing the current operation - case inProgress(fractionCompleted: Double, message: String) - - /// Operation queue is paused. - /// There may or may not be operations in the queue. - case paused - -} - -extension OperationQueueStatus: CustomStringConvertible { - - public var description: String { - - switch self { - case .idle: - return "idle" - - case .inProgress(let fractionCompleted, let message): - return "\(fractionCompleted) \(message.quoted)" - - case .paused: - return "paused" - } - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift deleted file mode 100644 index f4a8c5e..0000000 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ /dev/null @@ -1,320 +0,0 @@ -// -// BasicOperationQueue.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation -import OTAtomics - -/// **OTCore:** -/// An `OperationQueue` subclass with useful additions. -open class BasicOperationQueue: OperationQueue { - - /// **OTCore:** - /// Any time the queue completes all of its operations and returns to an empty queue, reset the progress object's total unit count to 0. - public final var resetProgressWhenFinished: Bool - - /// **OTCore:** - /// A reference to the `Operation` that was last added to the queue. Returns `nil` if the operation finished and no longer exists. - public final weak var lastAddedOperation: Operation? - - /// **OTCore:** - /// Operation queue type. Determines max concurrent operation count. - public final var operationQueueType: OperationQueueType { - didSet { - updateFromOperationQueueType() - } - } - - private func updateFromOperationQueueType() { - - switch operationQueueType { - case .serialFIFO: - maxConcurrentOperationCount = 1 - - case .concurrentAutomatic: - maxConcurrentOperationCount = OperationQueue.defaultMaxConcurrentOperationCount - - case .concurrent(let maxConcurrentOperations): - maxConcurrentOperationCount = maxConcurrentOperations - } - - } - - private var done = true - - // MARK: - Status - - @OTAtomicsThreadSafe private var _status: OperationQueueStatus = .idle - /// **OTCore:** - /// Operation queue status. - /// To observe changes to this value, supply a closure to the `statusHandler` property. - public internal(set) var status: OperationQueueStatus { - get { - _status - } - set { - let oldValue = _status - _status = newValue - - if newValue != oldValue { - DispatchQueue.main.async { - self.statusHandler?(newValue, oldValue) - } - } - } - } - - public typealias StatusHandler = (_ newStatus: OperationQueueStatus, - _ oldStatus: OperationQueueStatus) -> Void - - /// **OTCore:** - /// Handler called any time the `status` property changes. - /// Handler is called async on the main thread. - public final var statusHandler: StatusHandler? - - // MARK: - Progress Back-Porting - - private var _progress: Progress = .init() - - @available(macOS 10.9, iOS 7.0, tvOS 9.0, watchOS 2.0, *) - @objc dynamic - public override final var progress: Progress { _progress } - - // MARK: - Init - - /// **OTCore:** - /// Set max concurrent operation count. - /// Status handler is called async on the main thread. - public init(type operationQueueType: OperationQueueType, - resetProgressWhenFinished: Bool = false, - statusHandler: StatusHandler? = nil) { - - self.operationQueueType = operationQueueType - self.resetProgressWhenFinished = resetProgressWhenFinished - self.statusHandler = statusHandler - - super.init() - - updateFromOperationQueueType() - - addObservers() - - } - - // MARK: - Overrides - - /// **OTCore:** - /// Add an operation to the operation queue. - public final override func addOperation( - _ op: Operation - ) { - - switch operationQueueType { - case .serialFIFO: - // to enforce a serial queue, we add the previous operation as a dependency to the new one if it still exists - if let lastOp = lastAddedOperation { - op.addDependency(lastOp) - } - default: - break - } - - // update progress - if let basicOp = op as? BasicOperation { - progress.totalUnitCount += 100 - // give 100 units of progress in case child progress reports fractional progress - progress.addChild(basicOp.progress, - withPendingUnitCount: 100) - } else { - progress.totalUnitCount += 1 - } - - lastAddedOperation = op - - done = false - super.addOperation(op) - - } - - /// **OTCore:** - /// Add an operation block. - public final override func addOperation( - _ block: @escaping () -> Void - ) { - - // wrap in an actual operation object so we can track it - let op = ClosureOperation { - block() - } - addOperation(op) - - } - - /// **OTCore:** - /// Add operation blocks. - /// If queue type is Serial FIFO, operations will be added in array order. - public final override func addOperations( - _ ops: [Operation], - waitUntilFinished wait: Bool - ) { - guard !ops.isEmpty else { return } - - switch operationQueueType { - case .serialFIFO: - // to enforce a serial queue, we add the previous operation as a dependency to the new one if it still exists - var parentOperation: Operation? = lastAddedOperation - ops.forEach { - if let parentOperation = parentOperation { - $0.addDependency(parentOperation) - } - parentOperation = $0 - } - - default: - break - } - - // update progress - for op in ops { - if let basicOp = op as? BasicOperation { - progress.totalUnitCount += 100 - // give 100 units of progress in case child progress reports fractional progress - progress.addChild(basicOp.progress, - withPendingUnitCount: 100) - } else { - progress.totalUnitCount += 1 - } - } - - lastAddedOperation = ops.last - - done = false - super.addOperations(ops, waitUntilFinished: wait) - - } - - // MARK: - KVO Observers - - /// **OTCore:** - /// Retain property observers. For safety, this array must be emptied on class deinit. - private var observers: [NSKeyValueObservation] = [] - - private func addObservers() { - - // self.isSuspended - - observers.append( - observe(\.isSuspended, options: [.new]) - { [self, progress] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - if isSuspended { - status = .paused - } else { - if done { - setStatusIdle(resetProgress: resetProgressWhenFinished) - } else { - status = .inProgress( - fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription - ) - - } - } - } - ) - - // self.operationCount - - observers.append( - observe(\.operationCount, options: [.new]) - { [self, progress] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - guard !isSuspended else { return } - - if !done, - !progress.isFinished, - operationCount > 0 - { - status = .inProgress(fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription) - } else { - setStatusIdle(resetProgress: resetProgressWhenFinished) - } - } - ) - - // self.progress.fractionCompleted - // (NSProgress docs state that fractionCompleted is KVO-observable) - - observers.append( - progress.observe(\.fractionCompleted, options: [.new]) - { [self, progress] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - guard !isSuspended else { return } - - if done || - progress.isFinished || - progress.completedUnitCount == progress.totalUnitCount || - operationCount == 0 - { - setStatusIdle(resetProgress: resetProgressWhenFinished) - } else { - status = .inProgress(fractionCompleted: progress.fractionCompleted, - message: progress.localizedDescription) - } - } - ) - - // self.progress.isFinished - // (NSProgress docs state that isFinished is KVO-observable) - - observers.append( - progress.observe(\.isFinished, options: [.new]) - { [self, progress] _, _ in - // !!! DO NOT USE [weak self] HERE. MUST BE STRONG SELF !!! - - if progress.isFinished { - setStatusIdle(resetProgress: resetProgressWhenFinished) - } - } - ) - - } - - private func removeObservers() { - - observers.forEach { $0.invalidate() } // for extra safety, invalidate them first - observers.removeAll() - - } - - /// Only call as a result of the queue emptying - private func setStatusIdle(resetProgress: Bool) { - if resetProgress, - progress.totalUnitCount != 0, - progress.completedUnitCount != 0 - { - progress.totalUnitCount = 0 - progress.completedUnitCount = 0 - } - - done = true - status = .idle - } - - deinit { - - // this is very important or it may result in random crashes if the KVO observers aren't nuked at the appropriate time - removeObservers() - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/OperationQueue/OperationQueue Extensions.swift b/Sources/OTCore/Threading/OperationQueue/OperationQueue Extensions.swift deleted file mode 100644 index 27b26e5..0000000 --- a/Sources/OTCore/Threading/OperationQueue/OperationQueue Extensions.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// OperationQueue Extensions.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -extension OperationQueue { - - /// **OTCore:** - /// Blocks the current thread until all the receiver’s queued and executing operations finish executing. Same as calling `waitUntilAllOperationsAreFinished()` but offers a timeout duration. - @discardableResult - public func waitUntilAllOperationsAreFinished( - timeout: DispatchTimeInterval - ) -> DispatchTimeoutResult { - - DispatchGroup.sync(asyncOn: .global(), - timeout: timeout) { g in - - self.waitUntilAllOperationsAreFinished() - g.leave() - - } - - } - -} - -#endif diff --git a/Sources/OTCore/Threading/OperationQueue/OperationQueueType.swift b/Sources/OTCore/Threading/OperationQueue/OperationQueueType.swift deleted file mode 100644 index 0b30a3f..0000000 --- a/Sources/OTCore/Threading/OperationQueue/OperationQueueType.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// OperationQueueType.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -public enum OperationQueueType { - - /// Serial (one operation at a time), FIFO (first-in-first-out). - case serialFIFO - - /// Concurrent operations. - /// Max number of concurrent operations will be automatically determined by the system. - case concurrentAutomatic - - /// Concurrent operations. - /// Specify the number of max concurrent operations. - case concurrent(max: Int) - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift deleted file mode 100644 index 4e76b4e..0000000 --- a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// BlockOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import XCTest -@testable import OTCore -import OTAtomics - -final class BlockOperation_Tests: XCTestCase { - - @OTAtomicsThreadSafe fileprivate var arr: [Int] = [] - - /// This does not test a feature of OTCore. Rather, it tests the behavior of Foundation's built-in `BlockOperation` object. - func testBlockOperation() { - - let op = BlockOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - for val in 1...100 { // will multi-thread - op.addExecutionBlock { - sleep(0.1) - self.arr.append(val) - } - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - // check that all operations executed. - // sort them first because BlockOperation execution blocks run concurrently and may be out-of-sequence - XCTAssertEqual(arr.sorted(), Array(1...100)) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - wait(for: [completionBlockExp], timeout: 2) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift deleted file mode 100644 index e8df6d6..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ /dev/null @@ -1,245 +0,0 @@ -// -// AsyncClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_AsyncClosureOperation_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - /// Test as a standalone operation. Run it. - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = AsyncClosureOperation { - mainBlockExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Do not run it. - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let op = AsyncClosureOperation { - mainBlockExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Run it. Cancel before it finishes. - func testOpRun_Cancel() { - - let mainBlockExp = expectation(description: "Main Block Called") - - // the operation's main block does finish eventually but won't finish in time for our timeout because there's no opportunity to return early from cancelling the operation - let mainBlockFinishedExp = expectation(description: "Main Block Finished") - mainBlockFinishedExp.isInverted = true - - // the operation's completion block does not fire in time because there's no opportunity to return early from cancelling the operation - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let op = AsyncClosureOperation(on: .global()) { - mainBlockExp.fulfill() - sleep(4) // seconds - mainBlockFinishedExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - sleep(0.1) - op.cancel() // cancel the operation directly (since we are not using an OperationQueue) - - wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - - sleep(0.2) // give a little time for cleanup - - // state - XCTAssertFalse(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertTrue(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertLessThan(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = AsyncClosureOperation { - mainBlockExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue_Cancel() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - // the operation's main block does finish eventually but won't finish in time for our timeout because there's no opportunity to return early from cancelling the operation - let mainBlockFinishedExp = expectation(description: "Main Block Finished") - - // the operation's completion block does not fire in time because there's no opportunity to return early from cancelling the operation - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let op = AsyncClosureOperation(on: .global()) { - mainBlockExp.fulfill() - sleep(1) // seconds - mainBlockFinishedExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - - XCTAssertEqual(op.progress.totalUnitCount, 1) - XCTAssertEqual(opQ.progress.totalUnitCount, 1) - - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - sleep(0.1) - opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.4) - - // state - // the operation is still running because it cannot return early from being cancelled - XCTAssertEqual(opQ.operationCount, 1) - XCTAssertFalse(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertTrue(op.isExecuting) // still executing - // progress - operation - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) // even if the async op is still running, this will be true now - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - wait(for: [mainBlockFinishedExp], timeout: 0.7) - wait(for: opQ.operationCount == 0, timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift deleted file mode 100644 index b3c835b..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift +++ /dev/null @@ -1,220 +0,0 @@ -// -// ClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_ClosureOperation_Tests: XCTestCase { - - override func setUpWithError() throws { - mainCheck = { } - } - - private var mainCheck: () -> Void = { } - - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = ClosureOperation { - self.mainCheck() - } - - // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure - mainCheck = { - mainBlockExp.fulfill() - XCTAssertTrue(op.isExecuting) - } - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let op = ClosureOperation { - self.mainCheck() - } - - // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure - mainCheck = { - mainBlockExp.fulfill() - XCTAssertTrue(op.isExecuting) - } - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Cancel it before it runs. - func testOpCancelBeforeRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let op = ClosureOperation { - self.mainCheck() - } - - // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure - mainCheck = { - mainBlockExp.fulfill() - XCTAssertTrue(op.isExecuting) - } - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.cancel() - op.start() // in an OperationQueue, all operations must start even if they're already cancelled - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = ClosureOperation { - self.mainCheck() - } - - // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure - mainCheck = { - mainBlockExp.fulfill() - XCTAssertTrue(op.isExecuting) - } - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - - /// Test that start() runs synchronously. Run it. - func testOp_SynchronousTest_Run() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - var val = 0 - - let op = ClosureOperation { - self.mainCheck() - sleep(0.5) - val = 1 - } - - // have to define this after ClosureOperation is initialized, since it can't reference itself in its own initializer closure - mainCheck = { - mainBlockExp.fulfill() - XCTAssertTrue(op.isExecuting) - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - // state - XCTAssertEqual(val, 1) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 2) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift deleted file mode 100644 index 0869429..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveAsyncClosureOperation Tests.swift +++ /dev/null @@ -1,242 +0,0 @@ -// -// InteractiveAsyncClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_InteractiveAsyncClosureOperation_Tests: XCTestCase { - - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = InteractiveAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let op = InteractiveAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Run it. Cancel before it finishes. - func testOpRun_Cancel() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = InteractiveAsyncClosureOperation(on: .global()) { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - operation.progress.totalUnitCount = 100 - - for i in 1...100 { // finishes in 20 seconds - operation.progress.completedUnitCount = Int64(i) - - sleep(0.2) - - // would call this once ore more throughout the operation - if operation.mainShouldAbort() { return } - } - - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - sleep(0.1) - op.cancel() // cancel the operation directly (since we are not using an OperationQueue) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) - XCTAssertLessThan(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let mainBlockFinishedExp = expectation(description: "Main Block Finished") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = InteractiveAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - mainBlockFinishedExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue_Cancel() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - // main block does not finish because we return early from cancelling the operation - let mainBlockFinishedExp = expectation(description: "Main Block Finished") - - // completion block still successfully fires because our early return from being cancelled marks the operation as isFinished == true - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = InteractiveAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - defer { mainBlockFinishedExp.fulfill() } - - operation.progress.totalUnitCount = 100 - - for i in 1...100 { // finishes in 20 seconds - operation.progress.completedUnitCount = Int64(i) - - sleep(0.2) - - // would call this once ore more throughout the operation - if operation.mainShouldAbort() { return } - } - - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - sleep(0.1) - opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - - wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) - XCTAssertLessThan(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift deleted file mode 100644 index 49b9be5..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/InteractiveClosureOperation Tests.swift +++ /dev/null @@ -1,145 +0,0 @@ -// -// InteractiveClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_InteractiveClosureOperation_Tests: XCTestCase { - - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = InteractiveClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - // do some work... - if operation.mainShouldAbort() { return } - // do some work... - } - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let op = InteractiveClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - // do some work... - if operation.mainShouldAbort() { return } - // do some work... - } - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let opQ = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = InteractiveClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - // do some work... - if operation.mainShouldAbort() { return } - // do some work... - } - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(opQ.operationCount, 0) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Test that start() runs synchronously. Run it. - func testOp_SynchronousTest_Run() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - var val = 0 - - let op = InteractiveClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - sleep(0.5) - val = 1 - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - XCTAssertEqual(val, 1) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 2) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift deleted file mode 100644 index 0801055..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ /dev/null @@ -1,489 +0,0 @@ -// -// AtomicBlockOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -@testable import OTCore -import XCTest - -final class Threading_AtomicBlockOperation_Tests: XCTestCase { - - func testEmpty() { - - let op = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int : [Int]]()) - - op.start() - - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Standalone operation, serial FIFO queue mode. Run it. - func testOp_serialFIFO_Run() { - - let op = AtomicBlockOperation(type: .serialFIFO, - initialMutableValue: [Int]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - let dataVerificationExp = expectation(description: "Data Verification") - - for val in 1...100 { - op.addOperation { $0.mutate { $0.append(val) } } - } - - op.setCompletionBlock { v in - completionBlockExp.fulfill() - - // check that all operations executed and they are in serial FIFO order - v.mutate { value in - XCTAssertEqual(value, Array(1...100)) - dataVerificationExp.fulfill() - } - } - - op.start() - - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Standalone operation, concurrent threading queue mode. Run it. - func testOp_concurrentAutomatic_Run() { - - let op = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - let dataVerificationExp = expectation(description: "Data Verification") - - for val in 1...100 { - op.addOperation { $0.mutate { $0.append(val) } } - } - - op.setCompletionBlock { v in - completionBlockExp.fulfill() - - v.mutate { value in - // check that all operations executed - XCTAssertEqual(value.count, 100) - - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(value.contains)) - - dataVerificationExp.fulfill() - } - } - - op.start() - - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - - XCTAssertEqual(op.value.count, 100) - XCTAssert(Array(1...100).allSatisfy(op.value.contains)) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Test as a standalone operation. Do not run it. - func testOp_concurrentAutomatic_NotRun() { - - let op = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let dataVerificationExp = expectation(description: "Data Verification") - dataVerificationExp.isInverted = true - - for val in 1...100 { - op.addOperation { $0.mutate { $0.append(val) } } - } - - op.setCompletionBlock { v in - completionBlockExp.fulfill() - - v.mutate { value in - // check that all operations executed - XCTAssertEqual(value.count, 100) - - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(value.contains)) - - dataVerificationExp.fulfill() - } - } - - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - - // state - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Standalone operation, concurrent threading queue mode. Run it. - func testOp_concurrentSpecificMax_Run() { - - let op = AtomicBlockOperation(type: .concurrent(max: 10), - initialMutableValue: [Int]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - let dataVerificationExp = expectation(description: "Data Verification") - - let atomicBlockCompletedExp = expectation(description: "AtomicBlockOperation Completed") - - for val in 1...100 { - op.addOperation { $0.mutate { $0.append(val) } } - } - - op.setCompletionBlock { v in - completionBlockExp.fulfill() - - v.mutate { value in - // check that all operations executed - XCTAssertEqual(value.count, 100) - - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(value.contains)) - - dataVerificationExp.fulfill() - } - } - - DispatchQueue.global().async { - op.start() - atomicBlockCompletedExp.fulfill() - } - - wait(for: [completionBlockExp, dataVerificationExp, atomicBlockCompletedExp], timeout: 1) - - XCTAssertEqual(op.value.count, 100) - XCTAssert(Array(1...100).allSatisfy(op.value.contains)) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testOp_concurrentAutomatic_Queue() { - - let opQ = OperationQueue() - - let op = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - // test default qualityOfService to check baseline state - XCTAssertEqual(op.qualityOfService, .default) - - op.qualityOfService = .userInitiated - - let completionBlockExp = expectation(description: "Completion Block Called") - - let dataVerificationExp = expectation(description: "Data Verification") - - for val in 1...100 { - op.addOperation { v in - // QoS should be inherited from the AtomicBlockOperation QoS - XCTAssertEqual(Thread.current.qualityOfService, .userInitiated) - - // add value to array - v.mutate { $0.append(val) } - } - } - - op.setCompletionBlock { v in - completionBlockExp.fulfill() - - v.mutate { value in - // check that all operations executed - XCTAssertEqual(value.count, 100) - - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(value.contains)) - - dataVerificationExp.fulfill() - } - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - } - - /// Standalone operation, serial FIFO queue mode. Test that start() runs synchronously. Run it. - func testOp_serialFIFO_SynchronousTest_Run() { - - let op = AtomicBlockOperation(type: .serialFIFO, - initialMutableValue: [Int]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - for val in 1...100 { // will take 1 second to complete - op.addOperation { v in - sleep(0.01) - v.mutate { $0.append(val) } - } - } - - op.setCompletionBlock { _ in - completionBlockExp.fulfill() - } - - op.start() - - // check that all operations executed and they are in serial FIFO order - XCTAssertEqual(op.value, Array(1...100)) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - - wait(for: [completionBlockExp], timeout: 2) - - } - - /// Test a `AtomicBlockOperation` that enqueues multiple `AtomicBlockOperation`s and ensure data mutability works as expected. - func testNested() { - - let mainOp = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int : [Int]]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - var mainVal: [Int : [Int]] = [:] - - for keyNum in 1...10 { - mainOp.addOperation { v in - let subOp = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - subOp.addOperation { v in - v.mutate { value in - for valueNum in 1...200 { - value.append(valueNum) - } - } - } - - subOp.start() - - v.mutate { value in - value[keyNum] = subOp.value - } - } - } - - mainOp.setCompletionBlock { v in - v.mutate { value in - mainVal = value - } - - completionBlockExp.fulfill() - } - - mainOp.start() - - wait(for: [completionBlockExp], timeout: 5) - - // state - XCTAssertTrue(mainOp.isFinished) - XCTAssertFalse(mainOp.isCancelled) - XCTAssertFalse(mainOp.isExecuting) - - XCTAssertEqual(mainVal.count, 10) - XCTAssertEqual(mainVal.keys.sorted(), Array(1...10)) - XCTAssert(mainVal.values.allSatisfy({ $0.sorted() == Array(1...200)})) - - } - - func testNested_Cancel() { - - let mainOp = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int : [Int]]()) - - let completionBlockExp = expectation(description: "Completion Block Called") - - var mainVal: [Int : [Int]] = [:] - - for keyNum in 1...10 { - let subOp = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - var refs: [Operation] = [] - for valueNum in 1...20 { - let ref = subOp.addInteractiveOperation { op, v in - if op.mainShouldAbort() { return } - sleep(0.2) - v.mutate { value in - value.append(valueNum) - } - } - refs.append(ref) - } - - subOp.addOperation { [weak mainOp] v in - var getVal: [Int] = [] - v.mutate { value in - getVal = value - } - mainOp?.mutateValue { mainValue in - mainValue[keyNum] = getVal - } - } - - mainOp.addOperation(subOp) - } - - mainOp.setCompletionBlock { v in - v.mutate { value in - mainVal = value - } - - completionBlockExp.fulfill() - } - - // must run start() async in order to cancel it, since - // the operation is synchronous and will complete before we - // can call cancel() if start() is run in-thread - DispatchQueue.global().async { - mainOp.start() - } - sleep(0.1) - mainOp.cancel() - - wait(for: [completionBlockExp], timeout: 1) - - //XCTAssertEqual(mainOp.operationQueue.operationCount, 0) - - // state - XCTAssertTrue(mainOp.isFinished) - XCTAssertTrue(mainOp.isCancelled) - XCTAssertFalse(mainOp.isExecuting) // TODO: technically this should be true, but it gets set to false when the completion method gets called even if async code is still running - - let expectedArray = (1...10).reduce(into: [Int: [Int]]()) { - $0[$1] = Array(1...200) - } - XCTAssertNotEqual(mainVal, expectedArray) - - } - - /// Ensure that nested progress objects successfully result in the topmost queue calling statusHandler at every increment of all progress children at every level. - func testProgress() { - - class OpQProgressTest { - var statuses: [OperationQueueStatus] = [] - - let mainOp = AtomicOperationQueue(type: .serialFIFO, - qualityOfService: .default, - initiallySuspended: true, - resetProgressWhenFinished: true, - initialMutableValue: 0) - - init() { - mainOp.statusHandler = { newStatus, oldStatus in - if self.statuses.isEmpty { - self.statuses.append(oldStatus) - print("-", oldStatus) - } - self.statuses.append(newStatus) - print("-", newStatus) - } - } - } - - let ppQProgressTest = OpQProgressTest() - - func runTest() { - // 5 ops, each with 2 ops, each with 2 units of progress. - // should equate to 20 total main progress updates 5% apart - for _ in 1...5 { - let subOp = AtomicBlockOperation(type: .serialFIFO, - initialMutableValue: 0) - - for _ in 1...2 { - subOp.addInteractiveOperation { operation, atomicValue in - operation.progress.totalUnitCount = 2 - - operation.progress.completedUnitCount = 1 - operation.progress.completedUnitCount = 2 - } - } - - ppQProgressTest.mainOp.addOperation(subOp) - } - - ppQProgressTest.mainOp.isSuspended = false - - wait(for: ppQProgressTest.mainOp.status == .idle, timeout: 2.0) - } - - let runExp = expectation(description: "Test Run") - DispatchQueue.global().async { - runTest() - runExp.fulfill() - } - wait(for: [runExp], timeout: 5.0) - - XCTAssertEqual(ppQProgressTest.statuses, [ - .idle, - .paused, - .inProgress(fractionCompleted: 0.00, message: "0% completed"), - .inProgress(fractionCompleted: 0.05, message: "5% completed"), - .inProgress(fractionCompleted: 0.10, message: "10% completed"), - .inProgress(fractionCompleted: 0.15, message: "15% completed"), - .inProgress(fractionCompleted: 0.20, message: "20% completed"), - .inProgress(fractionCompleted: 0.25, message: "25% completed"), - .inProgress(fractionCompleted: 0.30, message: "30% completed"), - .inProgress(fractionCompleted: 0.35, message: "35% completed"), - .inProgress(fractionCompleted: 0.40, message: "40% completed"), - .inProgress(fractionCompleted: 0.45, message: "45% completed"), - .inProgress(fractionCompleted: 0.50, message: "50% completed"), - .inProgress(fractionCompleted: 0.55, message: "55% completed"), - .inProgress(fractionCompleted: 0.60, message: "60% completed"), - .inProgress(fractionCompleted: 0.65, message: "65% completed"), - .inProgress(fractionCompleted: 0.70, message: "70% completed"), - .inProgress(fractionCompleted: 0.75, message: "75% completed"), - .inProgress(fractionCompleted: 0.80, message: "80% completed"), - .inProgress(fractionCompleted: 0.85, message: "85% completed"), - .inProgress(fractionCompleted: 0.90, message: "90% completed"), - .inProgress(fractionCompleted: 0.95, message: "95% completed"), - .idle - ]) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift deleted file mode 100644 index 52dff19..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ /dev/null @@ -1,252 +0,0 @@ -// -// BasicAsyncOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_BasicAsyncOperation_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - // MARK: - Classes - - /// `BasicAsyncOperation` is designed to be subclassed. - /// This is a simple subclass to test. - private class TestBasicAsyncOperation: BasicAsyncOperation { - - override func main() { - - print("Starting main()") - guard mainShouldStart() else { return } - - XCTAssertTrue(isExecuting) - - // run a simple non-blocking loop that can frequently check for cancellation - - DispatchQueue.global().async { - // it's good to call this once or more throughout the operation - // so we can return early if the operation is cancelled - if self.mainShouldAbort() { return } - - self.completeOperation() - } - - } - - } - - /// `BasicAsyncOperation` is designed to be subclassed. - /// This is a simple subclass to test. - private class TestLongRunningBasicAsyncOperation: BasicAsyncOperation { - - private let totalOpCount = 100 - - override init() { - super.init() - progress.totalUnitCount = Int64(totalOpCount) - } - - override func main() { - - print("Starting main()") - guard mainShouldStart() else { return } - - XCTAssertTrue(isExecuting) - - // run a simple non-blocking loop that can frequently check for cancellation - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - - for opNum in 1...self.totalOpCount { // finishes in 20 seconds - // it's good to call this once or more throughout the operation - // so we can return early if the operation is cancelled - if self.mainShouldAbort() { return } - - self.progress.completedUnitCount = Int64(opNum) - - sleep(0.2) - } - - self.completeOperation() - } - - } - - } - - // MARK: - Tests - - /// Test as a standalone operation. Run it. - func testOpRun() { - - let op = TestBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Do not run it. - func testOpNotRun() { - - let op = TestBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Run it. Cancel before it finishes. - func testOpRun_Cancel() { - - let op = TestLongRunningBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - sleep(0.1) - op.cancel() // cancel the operation directly (since we are not using an OperationQueue) - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) - XCTAssertLessThan(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue() { - - let opQ = OperationQueue() - - let op = TestBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue_Cancel() { - - let opQ = OperationQueue() - - let op = TestLongRunningBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - sleep(0.1) - opQ.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertGreaterThan(op.progress.fractionCompleted, 0.0) - XCTAssertLessThan(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift deleted file mode 100644 index 69dd193..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift +++ /dev/null @@ -1,232 +0,0 @@ -// -// BasicOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_BasicOperation_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - // MARK: - Classes - - /// `BasicOperation` is designed to be subclassed. - /// This is a simple subclass to test. - private class TestBasicOperation: BasicOperation { - - override func main() { - - print("Starting main()") - guard mainShouldStart() else { return } - - XCTAssertTrue(isExecuting) - - // it's good to call this once or more throughout the operation - // but it does nothing here since we're not asking this class to cancel - if mainShouldAbort() { return } - - completeOperation() - - } - - } - - /// `BasicOperation` is designed to be subclassed. - /// This is a simple subclass to test. - private class TestDelayedMutatingBasicOperation: BasicOperation { - - public var val: Int - private var valChangeTo: Int - - public init(initial: Int, - changeTo: Int) { - - val = initial - valChangeTo = changeTo - super.init() - - } - - override func main() { - - print("Starting main()") - guard mainShouldStart() else { return } - - XCTAssertTrue(isExecuting) - - // it's good to call this once or more throughout the operation - // but it does nothing here since we're not asking this class to cancel - if mainShouldAbort() { return } - - sleep(0.5) - val = valChangeTo - - completeOperation() - - } - - } - - // MARK: - Tests - - /// Test as a standalone operation. Run it. - func testOpRun() { - - let op = TestBasicOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Do not run it. - func testOpNotRun() { - - let op = TestBasicOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test as a standalone operation. Cancel it before it runs. - func testOpCancelBeforeRun() { - - let op = TestBasicOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.cancel() - op.start() // in an OperationQueue, all operations must start even if they're already cancelled - - wait(for: [completionBlockExp], timeout: 0.3) - - // state - XCTAssertTrue(op.isReady) - XCTAssertTrue(op.isFinished) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertFalse(op.progress.isFinished) - XCTAssertTrue(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 0.0) - XCTAssertFalse(op.progress.isIndeterminate) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) - func testQueue() { - - let opQ = OperationQueue() - - let op = TestBasicOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // must manually increment for OperationQueue - opQ.progress.totalUnitCount += 1 - // queue automatically starts the operation once it's added - opQ.addOperation(op) - - wait(for: [completionBlockExp], timeout: 0.5) - - // state - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - operation - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - // progress - queue - XCTAssertTrue(opQ.progress.isFinished) - XCTAssertFalse(opQ.progress.isCancelled) - XCTAssertEqual(opQ.progress.fractionCompleted, 1.0) - XCTAssertFalse(opQ.progress.isIndeterminate) - - } - - /// Test that start() runs synchronously. Run it. - func testOp_SynchronousTest_Run() { - - let completionBlockExp = expectation(description: "Completion Block Called") - - // after start(), will mutate self after 500ms then finish - let op = TestDelayedMutatingBasicOperation(initial: 0, - changeTo: 1) - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - XCTAssertEqual(op.val, 1) - - // state - XCTAssertTrue(op.isFinished) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - // progress - XCTAssertTrue(op.progress.isFinished) - XCTAssertFalse(op.progress.isCancelled) - XCTAssertEqual(op.progress.fractionCompleted, 1.0) - XCTAssertFalse(op.progress.isIndeterminate) - - wait(for: [completionBlockExp], timeout: 2) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift deleted file mode 100644 index fc403f0..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ /dev/null @@ -1,186 +0,0 @@ -// -// AtomicOperationQueue Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -@testable import OTCore -import XCTest - -final class Threading_AtomicOperationQueue_Tests: XCTestCase { - - /// Serial FIFO queue. - func testOp_serialFIFO_Run() { - - let opQ = AtomicOperationQueue(type: .serialFIFO, - initialMutableValue: [Int]()) - - for val in 1...100 { - opQ.addOperation { $0.mutate { $0.append(val) } } - } - - wait(for: opQ.status == .idle, timeout: 0.5) - //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - //XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(opQ.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertFalse(opQ.isSuspended) - XCTAssertEqual(opQ.status, .idle) - - } - - /// Concurrent automatic threading. Run it. - func testOp_concurrentAutomatic_Run() { - - let opQ = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - for val in 1...100 { - opQ.addOperation { $0.mutate { $0.append(val) } } - } - - wait(for: opQ.status == .idle, timeout: 0.5) - //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - //XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(opQ.sharedMutableValue.count, 100) - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertFalse(opQ.isSuspended) - XCTAssertEqual(opQ.status, .idle) - - } - - /// Concurrent automatic threading. Do not run it. Check status. Run it. Check status. - func testOp_concurrentAutomatic_Pause_Run() { - - let opQ = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - opQ.isSuspended = true - - XCTAssertEqual(opQ.status, .paused) - - for val in 1...100 { - opQ.addOperation { $0.mutate { $0.append(val) } } - } - - XCTAssertEqual(opQ.status, .paused) - - let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(200)) - XCTAssertEqual(timeoutResult, .timedOut) - - XCTAssertEqual(opQ.sharedMutableValue, []) - XCTAssertEqual(opQ.operationCount, 100) - XCTAssertTrue(opQ.isSuspended) - - wait(for: opQ.status == .paused, timeout: 0.1) - XCTAssertEqual(opQ.status, .paused) - - opQ.isSuspended = false - wait(for: (opQ.status != .paused && opQ.status != .idle), timeout: 0.2) - - wait(for: opQ.status == .idle, timeout: 2.0) - XCTAssertEqual(opQ.status, .idle) - - XCTAssertEqual(opQ.sharedMutableValue.count, 100) - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - - } - - /// Concurrent automatic threading. Run it. - func testOp_concurrentSpecific_Run() { - - let opQ = AtomicOperationQueue(type: .concurrent(max: 10), - initialMutableValue: [Int]()) - - for val in 1...100 { - opQ.addOperation { $0.mutate { $0.append(val) } } - } - - wait(for: opQ.status == .idle, timeout: 0.5) - //let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - //XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(opQ.sharedMutableValue.count, 100) - // this happens to be in serial order even though we are using concurrent threads and no operation dependencies are being used - XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertFalse(opQ.isSuspended) - XCTAssertEqual(opQ.status, .idle) - - } - - /// Serial FIFO queue. - /// Test the behavior of `addOperations()`. It should add operations in array order. - func testOp_serialFIFO_AddOperations_Run() { - - let opQ = AtomicOperationQueue(type: .serialFIFO, - initialMutableValue: [Int]()) - var ops: [Operation] = [] - - // first generate operation objects - for val in 1...50 { - let op = opQ.createOperation { $0.mutate { $0.append(val) } } - ops.append(op) - } - for val in 51...100 { - let op = opQ.createInteractiveOperation { _, v in - v.mutate { $0.append(val) } - } - ops.append(op) - } - - // then addOperations() with all 100 operations - opQ.addOperations(ops, waitUntilFinished: false) - - let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(opQ.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(opQ.sharedMutableValue.contains)) - - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertFalse(opQ.isSuspended) - XCTAssertEqual(opQ.status, .idle) - - } - - /// NOTE: this test similar to one in: BasicOperationQueue Tests.swift - func testResetProgressWhenFinished_True() { - - let opQ = AtomicOperationQueue(type: .serialFIFO, - resetProgressWhenFinished: true, - initialMutableValue: 0) // value doesn't matter - - for _ in 1...10 { - opQ.addInteractiveOperation { _,_ in sleep(0.1) } - } - - XCTAssertEqual(opQ.progress.totalUnitCount, 10 * 100) - - switch opQ.status { - case .inProgress(fractionCompleted: _, message: _): - break // correct - default: - XCTFail() - } - - wait(for: opQ.status == .idle, timeout: 1.5) - - wait(for: opQ.progress.totalUnitCount == 0, timeout: 0.5) - XCTAssertEqual(opQ.progress.totalUnitCount, 0) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift deleted file mode 100644 index e5b8717..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// BasicOperationQueue Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -@testable import OTCore -import XCTest - -final class Threading_BasicOperationQueue_Tests: XCTestCase { - - /// Serial FIFO queue. - func testOperationQueueType_serialFIFO() { - - let opQ = BasicOperationQueue(type: .serialFIFO) - - XCTAssertEqual(opQ.maxConcurrentOperationCount, 1) - - } - - /// Automatic concurrency. - func testOperationQueueType_automatic() { - - let opQ = BasicOperationQueue(type: .concurrentAutomatic) - - print(opQ.maxConcurrentOperationCount) - - XCTAssertEqual(opQ.maxConcurrentOperationCount, - OperationQueue.defaultMaxConcurrentOperationCount) - - } - - /// Specific number of concurrent operations. - func testOperationQueueType_specific() { - - let opQ = BasicOperationQueue(type: .concurrent(max: 2)) - - print(opQ.maxConcurrentOperationCount) - - XCTAssertEqual(opQ.maxConcurrentOperationCount, 2) - - } - - func testLastAddedOperation() { - - let opQ = BasicOperationQueue(type: .serialFIFO) - opQ.isSuspended = true - XCTAssertEqual(opQ.lastAddedOperation, nil) - - var op: Operation? = Operation() - opQ.addOperation(op!) - XCTAssertEqual(opQ.lastAddedOperation, op) - // just FYI: op.isFinished == false here - // but we don't care since it doesn't affect this test - - op = nil - opQ.isSuspended = false - wait(for: opQ.lastAddedOperation == nil, timeout: 0.5) - - } - - func testResetProgressWhenFinished_False() { - - let opQ = BasicOperationQueue(type: .serialFIFO, - resetProgressWhenFinished: false) - - for _ in 1...10 { - opQ.addOperation { } - } - - wait(for: opQ.status == .idle, timeout: 0.5) - wait(for: opQ.operationCount == 0, timeout: 0.5) - - XCTAssertEqual(opQ.progress.totalUnitCount, 10 * 100) - - } - - func testResetProgressWhenFinished_True() { - - class OpQProgressTest { - var statuses: [OperationQueueStatus] = [] - - let opQ = BasicOperationQueue(type: .serialFIFO, - resetProgressWhenFinished: true) - - init() { - opQ.statusHandler = { newStatus, oldStatus in - if self.statuses.isEmpty { - self.statuses.append(oldStatus) - print("-", oldStatus) - } - self.statuses.append(newStatus) - print("-", newStatus) - } - } - } - - let ppQProgressTest = OpQProgressTest() - - func runTen() { - print("Running 10 operations...") - for _ in 1...10 { - ppQProgressTest.opQ.addOperation { sleep(0.1) } - } - - XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 10 * 100) - - switch ppQProgressTest.opQ.status { - case .inProgress(fractionCompleted: _, message: _): - break // correct - default: - XCTFail() - } - - wait(for: ppQProgressTest.opQ.status == .idle, timeout: 1.5) - - wait(for: ppQProgressTest.opQ.progress.totalUnitCount == 0, timeout: 0.5) - XCTAssertEqual(ppQProgressTest.opQ.progress.completedUnitCount, 0) - XCTAssertEqual(ppQProgressTest.opQ.progress.totalUnitCount, 0) - } - - // run this global async, since the statusHandler gets called on main - let runExp = expectation(description: "Test Run") - DispatchQueue.global().async { - runTen() - runTen() - runExp.fulfill() - } - wait(for: [runExp], timeout: 5.0) - - XCTAssertEqual(ppQProgressTest.statuses, [ - .idle, - .inProgress(fractionCompleted: 0.0, message: "0% completed"), - .inProgress(fractionCompleted: 0.1, message: "10% completed"), - .inProgress(fractionCompleted: 0.2, message: "20% completed"), - .inProgress(fractionCompleted: 0.3, message: "30% completed"), - .inProgress(fractionCompleted: 0.4, message: "40% completed"), - .inProgress(fractionCompleted: 0.5, message: "50% completed"), - .inProgress(fractionCompleted: 0.6, message: "60% completed"), - .inProgress(fractionCompleted: 0.7, message: "70% completed"), - .inProgress(fractionCompleted: 0.8, message: "80% completed"), - .inProgress(fractionCompleted: 0.9, message: "90% completed"), - .idle, - .inProgress(fractionCompleted: 0.0, message: "0% completed"), - .inProgress(fractionCompleted: 0.1, message: "10% completed"), - .inProgress(fractionCompleted: 0.2, message: "20% completed"), - .inProgress(fractionCompleted: 0.3, message: "30% completed"), - .inProgress(fractionCompleted: 0.4, message: "40% completed"), - .inProgress(fractionCompleted: 0.5, message: "50% completed"), - .inProgress(fractionCompleted: 0.6, message: "60% completed"), - .inProgress(fractionCompleted: 0.7, message: "70% completed"), - .inProgress(fractionCompleted: 0.8, message: "80% completed"), - .inProgress(fractionCompleted: 0.9, message: "90% completed"), - .idle - ]) - - } - - func testStatus() { - - let opQ = BasicOperationQueue(type: .serialFIFO) - - opQ.statusHandler = { newStatus, oldStatus in - print(oldStatus, newStatus) - } - - XCTAssertEqual(opQ.status, .idle) - - let completionBlockExp = expectation(description: "Operation Completion") - - opQ.addOperation { - sleep(0.1) - completionBlockExp.fulfill() - } - - switch opQ.status { - case .inProgress(let fractionCompleted, let message): - XCTAssertEqual(fractionCompleted, 0.0) - _ = message // don't test message content, for now - default: - XCTFail() - } - - wait(for: [completionBlockExp], timeout: 0.5) - wait(for: opQ.operationCount == 0, timeout: 0.5) - wait(for: opQ.progress.isFinished, timeout: 0.5) - - XCTAssertEqual(opQ.status, .idle) - - opQ.isSuspended = true - - XCTAssertEqual(opQ.status, .paused) - - opQ.isSuspended = false - - XCTAssertEqual(opQ.status, .idle) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift deleted file mode 100644 index 33c974b..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// OperationQueue Extensions Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import XCTest -import OTCore -import OTAtomics - -final class Threading_OperationQueueExtensions_Success_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - @OTAtomicsThreadSafe fileprivate var val = 0 - - func testWaitUntilAllOperationsAreFinished_Timeout_Success() { - - let opQ = OperationQueue() - opQ.maxConcurrentOperationCount = 1 // serial - opQ.isSuspended = true - - val = 0 - - opQ.addOperation { - sleep(0.1) - self.val = 1 - } - - opQ.isSuspended = false - let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) - - XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(opQ.operationCount, 0) - XCTAssertEqual(val, 1) - - } - -} - -final class Threading_OperationQueueExtensions_TimedOut_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - @OTAtomicsThreadSafe fileprivate var val = 0 - - func testWaitUntilAllOperationsAreFinished_Timeout_TimedOut() { - - let opQ = OperationQueue() - opQ.maxConcurrentOperationCount = 1 // serial - opQ.isSuspended = true - - val = 0 - - opQ.addOperation { - sleep(1) - self.val = 1 - } - - opQ.isSuspended = false - let timeoutResult = opQ.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) - - XCTAssertEqual(timeoutResult, .timedOut) - XCTAssertEqual(opQ.operationCount, 1) - XCTAssertEqual(val, 0) - - } - -} - -#endif From f1e672401e297f58154c44015e07d245cf78a9ef Mon Sep 17 00:00:00 2001 From: Steffan Andrews Date: Mon, 14 Feb 2022 14:34:12 -0800 Subject: [PATCH 31/31] Fixed file header --- Tests/OTCoreTests/Utilities.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift index 7ccd115..ac4d574 100644 --- a/Tests/OTCoreTests/Utilities.swift +++ b/Tests/OTCoreTests/Utilities.swift @@ -1,5 +1,5 @@ // -// OTCoreTests.swift +// Utilities.swift // OTCore • https://github.com/orchetect/OTCore //