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"> { - - 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/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/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 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/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 5c88506..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/AsyncClosureOperation.swift +++ /dev/null @@ -1,85 +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. If you want to allow cancellation (early return partway through operation execution) use `CancellableAsyncClosureOperation` 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 ... -/// } -/// -/// // 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. -/// -/// // 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 mainStartOperation() 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/CancellableAsyncClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/CancellableAsyncClosureOperation.swift deleted file mode 100644 index a0d8ff4..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/CancellableAsyncClosureOperation.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// CancellableAsyncClosureOperation.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 `mainStartOperation()` at the start of the block, as the initial check is done for you internally. -/// -/// 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 -/// // ... do some work ... -/// -/// // 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 -/// operation.completeOperation() -/// } -/// -/// // 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 ... -/// -/// // finally call complete -/// operation.completeOperation() -/// } -/// -/// 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 CancellableAsyncClosureOperation: BasicAsyncOperation { - - public final let queue: DispatchQueue? - public final let mainBlock: (_ operation: CancellableAsyncClosureOperation) -> Void - - public init( - on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: CancellableAsyncClosureOperation) -> Void - ) { - - self.queue = queue - self.mainBlock = mainBlock - - } - - override public final func main() { - - guard mainStartOperation() 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/CancellableClosureOperation.swift b/Sources/OTCore/Threading/Operation/Closure/CancellableClosureOperation.swift deleted file mode 100644 index 8b7f067..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/CancellableClosureOperation.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// CancellableClosureOperation.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. -/// -/// let op = CancellableClosureOperation { operation in -/// // ... do some work ... -/// -/// // 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 CancellableClosureOperation: BasicOperation { - - public final override var isAsynchronous: Bool { false } - - public final var mainBlock: (_ operation: CancellableClosureOperation) -> Void - - public init(_ mainBlock: @escaping (_ operation: CancellableClosureOperation) -> Void) { - - self.mainBlock = mainBlock - - } - - override public func main() { - - guard mainStartOperation() else { return } - mainBlock(self) - 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 83e8046..0000000 --- a/Sources/OTCore/Threading/Operation/Closure/ClosureOperation.swift +++ /dev/null @@ -1,59 +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. If you want to allow cancellation (early return partway through operation execution) use `CancellableClosureOperation` instead. -/// -/// let op = ClosureOperation { -/// // ... 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 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 mainStartOperation() else { return } - mainBlock() - 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 458c7b1..0000000 --- a/Sources/OTCore/Threading/Operation/Complex/AtomicBlockOperation.swift +++ /dev/null @@ -1,288 +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.addCancellableOperation { 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 { - - private var operationQueueType: OperationQueueType { - operationQueue.operationQueueType - } - - private let operationQueue: AtomicOperationQueue - - public weak var lastAddedOperation: Operation? { - operationQueue.lastAddedOperation - } - - /// 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) { - - block(&operationQueue.sharedMutableValue) - - } - - private var setupBlock: ((_ operation: AtomicBlockOperation, - _ atomicValue: AtomicVariableAccess) -> Void)? - - // MARK: - Init - - public init(type operationQueueType: OperationQueueType, - initialMutableValue: T) { - - // assign properties - self.operationQueue = AtomicOperationQueue( - type: operationQueueType, - initialMutableValue: initialMutableValue - ) - - // super - super.init() - - // set up queue - - operationQueue.isSuspended = true - - operationQueue.qualityOfService = qualityOfService - - // set up observers - addObservers() - - } - - // MARK: - Overrides - - public final override func main() { - - guard mainStartOperation() 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 { - usleep(10_000) // 10 milliseconds - //RunLoop.current.run(until: Date().addingTimeInterval(0.010)) // DO NOT DO THIS!!! - } - - } - - // MARK: - KVO Observers - - /// **OTCore:** - /// Retain property observers. For safety, this array must be emptied on class deinit. - 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() - } - } - 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) - - // 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() - } - } - observers.append(opCtRetain) - - } - - 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() - } - -} - -// 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 addCancellableOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ operation: CancellableClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { - - operationQueue.addCancellableOperation(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) - - } - - /// 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 87cf9cd..0000000 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicAsyncOperation.swift +++ /dev/null @@ -1,50 +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. -/// -/// 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** -/// -/// 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 mainStartOperation() 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 5515976..0000000 --- a/Sources/OTCore/Threading/Operation/Foundational/BasicOperation.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// BasicOperation.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **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 mainStartOperation() 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 { - - // MARK: - KVO - - // adding KVO compliance - public final override var isExecuting: Bool { _isExecuting } - @Atomic private var _isExecuting = false { - willSet { willChangeValue(for: \.isExecuting) } - didSet { didChangeValue(for: \.isExecuting) } - } - - // adding KVO compliance - public final override var isFinished: Bool { _isFinished } - @Atomic private var _isFinished = false { - willSet { willChangeValue(for: \.isFinished) } - didSet { didChangeValue(for: \.isFinished) } - } - - // adding KVO compliance - @objc dynamic - public final override var qualityOfService: QualityOfService { - willSet { willChangeValue(for: \.qualityOfService) } - didSet { didChangeValue(for: \.qualityOfService) } - } - - // MARK: - Method Overrides - - public final override func start() { - if isCancelled { completeOperation() } - super.start() - } - - // MARK: - Methods - - /// Returns true if operation should begin. - public final func mainStartOperation() -> Bool { - - guard !isCancelled else { - completeOperation() - return false - } - - guard !isExecuting else { return false } - _isExecuting = true - return true - - } - - /// Call this once all execution is complete in the operation. - public final func completeOperation() { - - _isExecuting = false - _isFinished = true - - } - - /// Checks if `isCancelled` is true, and calls `completedOperation()` if so. - /// Returns `isCancelled`. - public final func mainShouldAbort() -> Bool { - - if isCancelled { - completeOperation() - } - 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 37ae881..0000000 --- a/Sources/OTCore/Threading/Operation/Operation Extensions.swift +++ /dev/null @@ -1,58 +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 `CancellableClosureOperation`. - public static func cancellable( - _ mainBlock: @escaping (_ operation: CancellableClosureOperation) -> Void - ) -> CancellableClosureOperation { - - .init(mainBlock) - - } - - /// **OTCore:** - /// Convenience static constructor for `CancellableAsyncClosureOperation`. - public static func cancellableAsync( - on queue: DispatchQueue? = nil, - _ mainBlock: @escaping (_ operation: CancellableAsyncClosureOperation) -> Void - ) -> CancellableAsyncClosureOperation { - - .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 f9db94e..0000000 --- a/Sources/OTCore/Threading/OperationQueue/AtomicOperationQueue.swift +++ /dev/null @@ -1,131 +0,0 @@ -// -// AtomicOperationQueue.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **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. - @Atomic public final var sharedMutableValue: T - - // MARK: - Init - - public init( - type operationQueueType: OperationQueueType = .concurrentAutomatic, - initialMutableValue: T - ) { - - self.sharedMutableValue = initialMutableValue - - super.init(type: operationQueueType) - - } - - // 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 addCancellableOperation( - dependencies: [Operation] = [], - _ block: @escaping (_ operation: CancellableClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { - - let op = createCancellableOperation(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 createCancellableOperation( - _ block: @escaping (_ operation: CancellableClosureOperation, - _ atomicValue: AtomicVariableAccess) -> Void - ) -> CancellableClosureOperation { - - CancellableClosureOperation { [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.swift b/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift deleted file mode 100644 index 43e2934..0000000 --- a/Sources/OTCore/Threading/OperationQueue/BasicOperationQueue.swift +++ /dev/null @@ -1,124 +0,0 @@ -// -// BasicOperationQueue.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if canImport(Foundation) - -import Foundation - -/// **OTCore:** -/// An `OperationQueue` subclass with useful additions. -open class BasicOperationQueue: OperationQueue { - - /// **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 - } - - } - - // MARK: - Init - - /// **OTCore:** - /// Set max concurrent operation count. - public init(type operationQueueType: OperationQueueType) { - - self.operationQueueType = operationQueueType - - super.init() - - updateFromOperationQueueType() - - } - - // 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 - } - - lastAddedOperation = op - 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 - } - - lastAddedOperation = ops.last - - super.addOperations(ops, waitUntilFinished: wait) - - } - -} - -#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/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/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 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 diff --git a/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift b/Tests/OTCoreTests/Extensions/Foundation/DispatchGroup Tests.swift index c458a32..4d4900b 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) } @@ -209,9 +210,9 @@ 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 - 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 deleted file mode 100644 index d0c1a16..0000000 --- a/Tests/OTCoreTests/Threading/Operation/BlockOperation Tests.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// BlockOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -@testable import OTCore -import XCTest - -final class BlockOperation_Tests: XCTestCase { - - @Atomic 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 { - usleep(100_000) // milliseconds - 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 5221e17..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/AsyncClosureOperation Tests.swift +++ /dev/null @@ -1,180 +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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// 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() - usleep(100_000) // 100 milliseconds - 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 - - XCTAssertTrue(op.isCancelled) - XCTAssertTrue(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = AsyncClosureOperation { - mainBlockExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - func testQueue_Cancel() { - - let oq = 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") - completionBlockExp.isInverted = true - - let op = AsyncClosureOperation(on: .global()) { - mainBlockExp.fulfill() - sleep(4) // seconds - mainBlockFinishedExp.fulfill() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - usleep(100_000) // 100 milliseconds - oq.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 - - // the operation is still running because it cannot return early from being cancelled - XCTAssertEqual(oq.operationCount, 1) - - XCTAssertTrue(op.isCancelled) - XCTAssertTrue(op.isExecuting) // still executing - XCTAssertFalse(op.isFinished) // not yet finished - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift deleted file mode 100644 index b2f3244..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableAsyncClosureOperation Tests.swift +++ /dev/null @@ -1,190 +0,0 @@ -// -// CancellableAsyncClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_CancellableAsyncClosureOperation_Tests: XCTestCase { - - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = CancellableAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let completionBlockExp = expectation(description: "Completion Block Called") - completionBlockExp.isInverted = true - - let op = CancellableAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.3) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// 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 = CancellableAsyncClosureOperation(on: .global()) { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - for _ in 1...100 { // finishes in 20 seconds - usleep(200_000) // 200 milliseconds - // would call this once ore more throughout the operation - if operation.mainShouldAbort() { return } - } - - operation.completeOperation() - } - - op.completionBlock = { - 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) - - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let completionBlockExp = expectation(description: "Completion Block Called") - - let op = CancellableAsyncClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - func testQueue_Cancel() { - - let oq = 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 - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - - for _ in 1...100 { // finishes in 20 seconds - usleep(200_000) // 200 milliseconds - // would call this once ore more throughout the operation - if operation.mainShouldAbort() { return } - } - - mainBlockFinishedExp.fulfill() - operation.completeOperation() - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - usleep(100_000) // 100 milliseconds - oq.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - - wait(for: [mainBlockExp, mainBlockFinishedExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift b/Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift deleted file mode 100644 index 2076145..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/CancellableClosureOperation Tests.swift +++ /dev/null @@ -1,141 +0,0 @@ -// -// CancellableClosureOperation Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_CancellableClosureOperation_Tests: XCTestCase { - - func testOpRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = CancellableClosureOperation { 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - func testOpNotRun() { - - let mainBlockExp = expectation(description: "Main Block Called") - mainBlockExp.isInverted = true - - let op = CancellableClosureOperation { 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) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = OperationQueue() - - let mainBlockExp = expectation(description: "Main Block Called") - - let op = CancellableClosureOperation { 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 - oq.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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 = CancellableClosureOperation { operation in - mainBlockExp.fulfill() - XCTAssertTrue(operation.isExecuting) - usleep(500_000) // 500 milliseconds - val = 1 - } - - op.completionBlock = { - completionBlockExp.fulfill() - } - - op.start() - - XCTAssertEqual(val, 1) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 2) - - } - -} - -#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 8f94f13..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Closure/ClosureOperation Tests.swift +++ /dev/null @@ -1,155 +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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - 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) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = 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() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [mainBlockExp, completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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() - usleep(500_000) // 500 milliseconds - 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() - - XCTAssertEqual(val, 1) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - 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 4cb40cb..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Complex/AtomicBlockOperation Tests.swift +++ /dev/null @@ -1,385 +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() - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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)) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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") - - 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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") - - 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)) - - 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 op = AtomicBlockOperation(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - // test default qualityOfService to check baseline state - XCTAssertEqual(op.qualityOfService, .default) - - op.qualityOfService = .utility - - 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, .utility) - - // 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() - } - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [completionBlockExp, dataVerificationExp], timeout: 1) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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 - usleep(10_000) // milliseconds - 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)) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - 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: 10) - - XCTAssertFalse(mainOp.isCancelled) - XCTAssertFalse(mainOp.isExecuting) - XCTAssertTrue(mainOp.isFinished) - - 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.addCancellableOperation { op, v in - if op.mainShouldAbort() { return } - usleep(200_000) - 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() - } - - DispatchQueue.global().async { - mainOp.start() - } - usleep(100_000) // 100 milliseconds - mainOp.cancel() - - wait(for: [completionBlockExp], timeout: 5) - - //XCTAssertEqual(mainOp.operationQueue.operationCount, 0) - - XCTAssertTrue(mainOp.isCancelled) - XCTAssertFalse(mainOp.isExecuting) - XCTAssertTrue(mainOp.isFinished) - - XCTAssert(!mainVal.values.allSatisfy({ $0.sorted() == Array(1...200)})) - - dump(mainOp) - - } - -} - -#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 a048871..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicAsyncOperation Tests.swift +++ /dev/null @@ -1,195 +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 mainStartOperation() 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 { - - override func main() { - - print("Starting main()") - guard mainStartOperation() 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 - // 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() - } - - } - - } - - // 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// 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() - usleep(100_000) // 100 milliseconds - op.cancel() // cancel the operation directly (since we are not using an OperationQueue) - - wait(for: [completionBlockExp], timeout: 0.5) - - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = OperationQueue() - - let op = TestBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. Cancel before it finishes. - func testQueue_Cancel() { - - let oq = OperationQueue() - - let op = TestLongRunningBasicAsyncOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - usleep(100_000) // 100 milliseconds - oq.cancelAllOperations() // cancel the queue, not the operation. it cancels its operations. - - wait(for: [completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - -} - -#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 b4deaed..0000000 --- a/Tests/OTCoreTests/Threading/Operation/Foundational/BasicOperation Tests.swift +++ /dev/null @@ -1,190 +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 mainStartOperation() 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 - } - - override func main() { - - print("Starting main()") - guard mainStartOperation() 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 } - - usleep(500_000) // 500 milliseconds - 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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) - - XCTAssertTrue(op.isReady) - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertFalse(op.isFinished) - - } - - /// Test as a standalone operation. Do not run it. Cancel it before it runs. - func testOpNotRun_Cancel() { - - 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) - - XCTAssertTrue(op.isReady) - XCTAssertTrue(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// Test in the context of an OperationQueue. Run is implicit. - func testQueue() { - - let oq = OperationQueue() - - let op = TestBasicOperation() - - let completionBlockExp = expectation(description: "Completion Block Called") - - op.completionBlock = { - completionBlockExp.fulfill() - } - - // queue automatically starts the operation once it's added - oq.addOperation(op) - - wait(for: [completionBlockExp], timeout: 0.5) - - XCTAssertEqual(oq.operationCount, 0) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - } - - /// 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) - - XCTAssertFalse(op.isCancelled) - XCTAssertFalse(op.isExecuting) - XCTAssertTrue(op.isFinished) - - 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 09b374b..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/AtomicOperationQueue Tests.swift +++ /dev/null @@ -1,135 +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 oq = AtomicOperationQueue(type: .serialFIFO, - initialMutableValue: [Int]()) - - for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } - } - - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(oq.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) - - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) - - } - - /// Concurrent automatic threading. Run it. - func testOp_concurrentAutomatic_Run() { - - let oq = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } - } - - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(oq.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)) - - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) - - } - - /// Concurrent automatic threading. Do not run it. - func testOp_concurrentAutomatic_NotRun() { - - let oq = AtomicOperationQueue(type: .concurrentAutomatic, - initialMutableValue: [Int]()) - - oq.isSuspended = true - - for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } - } - - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .timedOut) - - XCTAssertEqual(oq.sharedMutableValue, []) - XCTAssertEqual(oq.operationCount, 100) - XCTAssertTrue(oq.isSuspended) - - } - - /// Concurrent automatic threading. Run it. - func testOp_concurrentSpecific_Run() { - - let oq = AtomicOperationQueue(type: .concurrent(max: 10), - initialMutableValue: [Int]()) - - for val in 1...100 { - oq.addOperation { $0.mutate { $0.append(val) } } - } - - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(oq.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)) - - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) - - } - - /// Serial FIFO queue. - /// Test the behavior of `addOperations()`. It should add operations in array order. - func testOp_serialFIFO_AddOperations_Run() { - - let oq = 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) } } - ops.append(op) - } - for val in 51...100 { - let op = oq.createCancellableOperation { _, v in - v.mutate { $0.append(val) } - } - ops.append(op) - } - - // then addOperations() with all 100 operations - oq.addOperations(ops, waitUntilFinished: false) - - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(timeoutResult, .success) - - XCTAssertEqual(oq.sharedMutableValue.count, 100) - XCTAssert(Array(1...100).allSatisfy(oq.sharedMutableValue.contains)) - - XCTAssertEqual(oq.operationCount, 0) - XCTAssertFalse(oq.isSuspended) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift b/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift deleted file mode 100644 index 6a00115..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/BasicOperationQueue Tests.swift +++ /dev/null @@ -1,64 +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 oq = BasicOperationQueue(type: .serialFIFO) - - XCTAssertEqual(oq.maxConcurrentOperationCount, 1) - - } - - /// Automatic concurrency. - func testOperationQueueType_automatic() { - - let oq = BasicOperationQueue(type: .concurrentAutomatic) - - print(oq.maxConcurrentOperationCount) - - XCTAssertEqual(oq.maxConcurrentOperationCount, - OperationQueue.defaultMaxConcurrentOperationCount) - - } - - /// Specific number of concurrent operations. - func testOperationQueueType_specific() { - - let oq = BasicOperationQueue(type: .concurrent(max: 2)) - - print(oq.maxConcurrentOperationCount) - - XCTAssertEqual(oq.maxConcurrentOperationCount, 2) - - } - - func testLastAddedOperation() { - - let oq = BasicOperationQueue(type: .serialFIFO) - oq.isSuspended = true - XCTAssertEqual(oq.lastAddedOperation, nil) - - var op: Operation? = Operation() - oq.addOperation(op!) - XCTAssertEqual(oq.lastAddedOperation, op) - - op = nil - oq.isSuspended = false - oq.waitUntilAllOperationsAreFinished(timeout: .seconds(1)) - XCTAssertEqual(oq.lastAddedOperation, nil) - - } - -} - -#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 515b0d9..0000000 --- a/Tests/OTCoreTests/Threading/OperationQueue/OperationQueue Extensions Tests.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// OperationQueue Extensions Tests.swift -// OTCore • https://github.com/orchetect/OTCore -// - -#if shouldTestCurrentPlatform - -import OTCore -import XCTest - -final class Threading_OperationQueueExtensions_Tests: XCTestCase { - - override func setUp() { super.setUp() } - override func tearDown() { super.tearDown() } - - func testWaitUntilAllOperationsAreFinished_Timeout_Success() { - - let oq = OperationQueue() - oq.maxConcurrentOperationCount = 1 // serial - oq.isSuspended = true - - var val = 0 - - oq.addOperation { - usleep(100_000) // 100 milliseconds - val = 1 - } - - oq.isSuspended = false - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .seconds(5)) - - XCTAssertEqual(timeoutResult, .success) - XCTAssertEqual(oq.operationCount, 0) - XCTAssertEqual(val, 1) - - } - - func testWaitUntilAllOperationsAreFinished_Timeout_TimedOut() { - - let oq = OperationQueue() - oq.maxConcurrentOperationCount = 1 // serial - oq.isSuspended = true - - var val = 0 - - oq.addOperation { - sleep(5) // 5 seconds - val = 1 - } - - oq.isSuspended = false - let timeoutResult = oq.waitUntilAllOperationsAreFinished(timeout: .milliseconds(500)) - - XCTAssertEqual(timeoutResult, .timedOut) - XCTAssertEqual(oq.operationCount, 1) - XCTAssertEqual(val, 0) - - } - -} - -#endif diff --git a/Tests/OTCoreTests/Utilities.swift b/Tests/OTCoreTests/Utilities.swift new file mode 100644 index 0000000..ac4d574 --- /dev/null +++ b/Tests/OTCoreTests/Utilities.swift @@ -0,0 +1,86 @@ +// +// Utilities.swift +// OTCore • https://github.com/orchetect/OTCore +// + +#if shouldTestCurrentPlatform + +import XCTest +import OTCore + +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) + + } + + #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() { + + var someString = "default string" + + DispatchQueue.global().async { + sleep(0.02) + someString = "new string" + } + + wait(for: someString == "new string", timeout: 0.3) + + } + +} + +#endif