├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .gitmodules ├── .spi.yml ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── OperationPlus │ ├── AsyncBlockConsumerOperation.swift │ ├── AsyncBlockConsumerProducerOperation.swift │ ├── AsyncBlockOperation.swift │ ├── AsyncBlockProducerOperation.swift │ ├── AsyncConsumerOperation.swift │ ├── AsyncConsumerProducerOperation.swift │ ├── AsyncOperation.swift │ ├── AsyncProducerOperation.swift │ ├── BaseOperation.swift │ ├── BaseOperationError.swift │ ├── BlockConsumerOperation.swift │ ├── BlockConsumerProducerOperation.swift │ ├── BlockProducerOperation.swift │ ├── ConsumerOperation.swift │ ├── ConsumerProducerOperation.swift │ ├── Extensions │ │ ├── Operation+Dependencies.swift │ │ ├── OperationQueue+Creation.swift │ │ ├── OperationQueue+Dependencies.swift │ │ ├── OperationQueue+Emptied.swift │ │ ├── OperationQueue+Enqueuing.swift │ │ ├── OperationQueue+Preconditions.swift │ │ └── Publishers.swift │ ├── OperationPlus.docc │ │ └── OperationPlus.md │ └── ProducerOperation.swift └── OperationTestingPlus │ ├── FulfillExpectationOperation.swift │ ├── NeverFinishingOperation.swift │ ├── NeverFinishingProducerOperation.swift │ └── OperationExpectation.swift └── Tests └── OperationPlusTests ├── AsyncBlockOperationTests.swift ├── BaseOperationTests.swift ├── CombineTests.swift ├── OperationPlusTests.xcconfig ├── OperationQueueTests.swift ├── ProducerConsumerTests.swift └── TestHelpers.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | test: 13 | name: Test 14 | runs-on: macOS-latest 15 | strategy: 16 | matrix: 17 | destination: 18 | - "platform=macOS" 19 | - "platform=iOS Simulator,name=iPhone 11" 20 | - "platform=tvOS Simulator,name=Apple TV" 21 | - "platform=watchOS Simulator,name=Apple Watch Series 6 (40mm)" 22 | 23 | steps: 24 | - uses: actions/checkout@v3 25 | - name: Test platform ${{ matrix.destination }} 26 | run: set -o pipefail && xcodebuild -scheme OperationPlus-Package -destination "${{ matrix.destination }}" test | xcpretty 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Carthage 2 | /Build 3 | .DS_Store 4 | /.build 5 | /Packages 6 | /.swiftpm 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "xcconfigs"] 2 | path = xcconfigs 3 | url = https://github.com/mrackwitz/xcconfigs.git 4 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documentation_targets: [OperationPlus] 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: swift 2 | os: osx 3 | osx_image: xcode10.2 4 | xcode_project: OperationPlus.xcodeproj 5 | 6 | install: 7 | - gem update cocoapods 8 | 9 | before_script: 10 | - pod lib lint --skip-tests --verbose 11 | 12 | matrix: 13 | include: 14 | - env: TEST_DESTINATION="platform=macOS" 15 | - env: TEST_DESTINATION="platform=iOS Simulator,name=iPhone 8" 16 | - env: TEST_DESTINATION="platform=tvOS Simulator,name=Apple TV" 17 | script: 18 | - xcodebuild test -scheme OperationPlus -destination "$TEST_DESTINATION" -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual 11 | identity and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the overall 27 | community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or advances of 32 | any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | support@chimehq.com. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series of 87 | actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or permanent 94 | ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within the 114 | community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.1, available at 120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 127 | [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, Chime 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "OperationPlus", 7 | platforms: [.macOS(.v10_10), .iOS(.v9), .tvOS(.v10), .watchOS(.v3)], 8 | products: [ 9 | .library(name: "OperationPlus", targets: ["OperationPlus"]), 10 | .library(name: "OperationTestingPlus", targets: ["OperationTestingPlus"]), 11 | ], 12 | dependencies: [], 13 | targets: [ 14 | .target(name: "OperationPlus", dependencies: []), 15 | .target(name: "OperationTestingPlus", dependencies: ["OperationPlus"]), 16 | .testTarget(name: "OperationPlusTests", dependencies: ["OperationPlus", "OperationTestingPlus"]), 17 | ], 18 | swiftLanguageVersions: [.v5] 19 | ) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | [![Build Status][build status badge]][build status] 4 | [![Platforms][platforms badge]][platforms] 5 | 6 |
7 | 8 | # OperationPlus 9 | 10 | OperationPlus is a set of `NSOperation` subclasses and extensions on `NSOperation`/`NSOperationQueue`. Its goal is to fill in the API's missing pieces. You don't need to learn anything new to use it. 11 | 12 | NSOperation has been around for a long time, and there are now two potential first-party alternatives, Combine and Swift concurrency. OperationPlus includes some facilities to help Combine and NSOperation interoperate conveniently. 13 | 14 | ## Integration 15 | 16 | Swift Package Manager: 17 | 18 | ```swift 19 | dependencies: [ 20 | .package(url: "https://github.com/ChimeHQ/OperationPlus.git") 21 | ] 22 | ``` 23 | 24 | ## NSOperation Subclasses 25 | 26 | - `BaseOperation`: provides core functionality for easier `NSOperation` subclassing 27 | - `AsyncOperation`: convenience wrapper around `BaseOperation` for async support 28 | - `AsyncBlockOperation`: convenience class for inline async support 29 | - `(Async)ProducerOperation`: produces an output 30 | - `(Async)ConsumerOperation`: accepts an input from a `ProducerOperation` 31 | - `(Async)ConsumerProducerOperation`: accepts an input from a `ProducerOperation` and also produces an output 32 | 33 | **BaseOperation** 34 | 35 | This is a simple `NSOperation` subclass built for easier extensibility. It features: 36 | 37 | - Thread-safety 38 | - Timeout support 39 | - Easier cancellation handling 40 | - Stricter state checking 41 | - Built-in asynchrous support 42 | - Straight-foward customization 43 | 44 | ```swift 45 | let a = BaseOperation(timeout: 5.0) 46 | 47 | // NSOperation will happily allow you do this even 48 | // if `a` has finished. `BaseOperation` will not. 49 | a.addDependency(another) 50 | 51 | // ... 52 | public override func main() { 53 | // This will return true if your operation is cancelled, timed out, 54 | // or prematurely finished. ProducerOperation subclass state will be 55 | // handled correctly as well. 56 | if self.checkForCancellation() { 57 | return 58 | } 59 | } 60 | // ... 61 | ``` 62 | 63 | **AsyncOperation** 64 | 65 | A `BaseOperation` subclass that can be used for your asynchronous operations. These are any operations that need to extend their lifetime past the `main` method. 66 | 67 | ```swift 68 | import Foundation 69 | import OperationPlus 70 | 71 | class MyAsyncOperation: AsyncOperation { 72 | public override func main() { 73 | DispatchQueue.global().async { 74 | if self.checkForCancellation() { 75 | return 76 | } 77 | 78 | // do stuff 79 | 80 | self.finish() 81 | } 82 | } 83 | } 84 | ``` 85 | 86 | There's also nothing special about this class at all -- it's there just for convenience. If you want, you can just subclass `BaseOperation` directly and override one method. 87 | 88 | ```swift 89 | import Foundation 90 | import OperationPlus 91 | 92 | class MyAsyncOperation: BaseOperation { 93 | override open var isAsynchronous: Bool { 94 | return true 95 | } 96 | } 97 | ``` 98 | 99 | **ProducerOperation** 100 | 101 | A `BaseOperation` subclass that yields a value. Includes a completion handler to access the value. 102 | 103 | ```swift 104 | import Foundation 105 | import OperationPlus 106 | 107 | class MyValueOperation: ProducerOperation { 108 | public override func main() { 109 | // do your computation 110 | 111 | self.finish(with: 42) 112 | } 113 | } 114 | 115 | // ... 116 | 117 | let op = MyValueOperation() 118 | 119 | op.resultCompletionBlock = { (value) in 120 | // use value here 121 | } 122 | ``` 123 | 124 | **AsyncProducerOperation** 125 | 126 | A variant of `ProducerOperation` that may produce a value after the `main` method has completed executing. 127 | 128 | ```swift 129 | import Foundation 130 | import OperationPlus 131 | 132 | class MyAsyncOperation: AsyncProducerOperation { 133 | public override func main() { 134 | DispatchQueue.global().async { 135 | if self.checkForCancellation() { 136 | return 137 | } 138 | 139 | // do stuff 140 | 141 | self.finish(with: 42) 142 | } 143 | } 144 | } 145 | ``` 146 | 147 | **ConsumerOperation** and **AsyncConsumerOperation** 148 | 149 | A `BaseOperation` sublass that accepts the input of a `ProducerOperation`. 150 | 151 | ```swift 152 | import Foundation 153 | import OperationPlus 154 | 155 | class MyConsumerOperation: ConsumerOperation { 156 | override func main() { 157 | guard let value = producerValue else { 158 | // handle failure in some way 159 | } 160 | } 161 | 162 | override func main(with value: Int) { 163 | // make use of value here, or automatically 164 | // fail if it wasn't successfully produced 165 | } 166 | } 167 | 168 | let op = MyConsumerOperation(producerOp: myIntProducerOperation) 169 | ``` 170 | 171 | **AsyncBlockOperation** 172 | 173 | A play on `NSBlockOperation`, but makes it possible to support asynchronous completion without making an `Operation` subclass. Great for quick, inline work. 174 | 175 | ```swift 176 | let op = AsyncBlockOperation { (completionBlock) in 177 | DispatchQueue.global().async { 178 | // do some async work here, just be certain to call 179 | // the completionBlock when done 180 | completionBlock() 181 | } 182 | } 183 | ``` 184 | 185 | ## NSOperation/NSOperationQueue Extensions 186 | 187 | Queue creation conveniences: 188 | 189 | ```swift 190 | let a = OperationQueue(name: "myqueue") 191 | let b = OperationQueue(name: "myqueue", maxConcurrentOperations: 1) 192 | let c = OperationQueue.serialQueue() 193 | let d = OperationQueue.serialQueue(named: "myqueue") 194 | ``` 195 | 196 | Enforcing runtime constraints on queue execution: 197 | 198 | ```swift 199 | OperationQueue.preconditionMain() 200 | OperationQueue.preconditionNotMain() 201 | ``` 202 | 203 | Consise dependencies: 204 | 205 | ```swift 206 | queue.addOperation(op, dependency: opA) 207 | queue.addOperation(op, dependencies: [opA, opB]) 208 | queue.addOperation(op, dependencies: Set([opA, opB])) 209 | 210 | op.addDependencies([opA, opB]) 211 | op.addDependencies(Set([opA, opB])) 212 | ``` 213 | 214 | Queueing work when a queue's current operations are complete: 215 | 216 | ```swift 217 | queue.currentOperationsFinished { 218 | print("all pending ops done") 219 | } 220 | ``` 221 | 222 | Convenient inline functions: 223 | 224 | ```swift 225 | queue.addAsyncOperation { (completionHandler) in 226 | DispatchQueue.global().async { 227 | // do some async work 228 | completionHandler() 229 | } 230 | } 231 | ``` 232 | 233 | Async integration: 234 | 235 | ```swift 236 | queue.addOperation { 237 | await asyncFunction1() 238 | await asyncFunction2() 239 | } 240 | 241 | let value = try await queue.addResultOperation { 242 | try await asyncValue() 243 | } 244 | ``` 245 | 246 | Delays: 247 | 248 | ```swift 249 | queue.addOperation(op, afterDelay: 5.0) 250 | queue.addOperation(afterDelay: 5.0) { 251 | // work 252 | } 253 | ``` 254 | 255 | ### Combine Integration 256 | 257 | **PublisherOperation** 258 | 259 | This `ProducerOperation` subclass takes a publisher. When executed, it creates a subscription and outputs the results. 260 | 261 | ```swift 262 | op.publisher() // AnyPublisher 263 | 264 | producerOp.outputPublisher() // AnyPublisher 265 | ``` 266 | 267 | ```swift 268 | publisher.operation() // PublisherOperation 269 | publisher.execute(on: queue) // subscribes and executes chain on queue and returns a publisher for result 270 | ``` 271 | 272 | ### XCTest Support 273 | 274 | **OperationTestingPlus** is an optional micro-framework to help make your XCTest-based tests a little nicer. When using Carthage, it is built as a static framework to help ease integration with your testing targets. 275 | 276 | **FulfillExpectationOperation** 277 | 278 | A simple NSOperation that will fulfill an `XCTestExpectation` when complete. Super-useful when used with dependencies on your other operations. 279 | 280 | **NeverFinishingOperation** 281 | 282 | A great way to test out your Operations' timeout behaviors. 283 | 284 | **OperationExpectation** 285 | 286 | An `XCTestExpectation` sublass to make testing async operations a little more XCTest-like. 287 | 288 | ```swift 289 | let op = NeverFinishingOperation() 290 | 291 | let expectation = OperationExpectation(operation: op) 292 | expectation.isInverted = true 293 | 294 | wait(for: [expectation], timeout: 1.0) 295 | ``` 296 | 297 | ### Suggestions or Feedback 298 | 299 | We'd love to hear from you! Get in touch via an issue or pull request. 300 | 301 | Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating in this project you agree to abide by its terms. 302 | 303 | [build status]: https://github.com/ChimeHQ/OperationPlus/actions 304 | [build status badge]: https://github.com/ChimeHQ/OperationPlus/workflows/CI/badge.svg 305 | [platforms]: https://swiftpackageindex.com/ChimeHQ/OperationPlus 306 | [platforms badge]: https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FChimeHQ%2FOperationPlus%2Fbadge%3Ftype%3Dplatforms 307 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncBlockConsumerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockConsumerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2020-03-23. 6 | // Copyright © 2020 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An asynchronous variant of BlockConsumerOperation 12 | public class AsyncBlockConsumerOperation: AsyncConsumerOperation { 13 | public typealias CompletionHandler = (Input, @escaping () -> Void) -> Void 14 | 15 | private let block: CompletionHandler 16 | 17 | public init(producerOp: ProducerOperation, timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping CompletionHandler) { 18 | self.block = block 19 | 20 | super.init(producerOp: producerOp, timeout: timeout) 21 | } 22 | 23 | override open func main(with producedValue: Input) { 24 | block(producedValue, { 25 | self.finish() 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncBlockConsumerProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockConsumerProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-14. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An asynchronous variant of BlockConsumerProducerOperation 12 | public class AsyncBlockConsumerProducerOperation: AsyncConsumerProducerOperation { 13 | public typealias CompletionHandler = (Input, @escaping (Output?) -> Void) -> Void 14 | 15 | private let block: CompletionHandler 16 | 17 | public init(producerOp: ProducerOperation, timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping CompletionHandler) { 18 | self.block = block 19 | 20 | super.init(producerOp: producerOp, timeout: timeout) 21 | } 22 | 23 | override open func main(with producedValue: Input) { 24 | block(producedValue, {(output) in 25 | if let o = output { 26 | self.finish(with: o) 27 | } else { 28 | self.finish() 29 | } 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncBlockOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-01-31. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An operation for enqueuing inline work that can be completed with a callback 12 | public class AsyncBlockOperation: AsyncOperation { 13 | public typealias CompletionHandler = (@escaping () -> Void) -> Void 14 | 15 | private let block: CompletionHandler 16 | 17 | public init(timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping CompletionHandler) { 18 | self.block = block 19 | 20 | super.init(timeout: timeout) 21 | } 22 | 23 | public override func main() { 24 | block({ 25 | self.finish() 26 | }) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncBlockProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-13. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An asynchronous variant of BlockProducerOperation 12 | public class AsyncBlockProducerOperation: AsyncProducerOperation { 13 | public typealias CompletionHandler = (@escaping (Output?) -> Void) -> Void 14 | 15 | private let block: CompletionHandler 16 | 17 | public init(timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping CompletionHandler) { 18 | self.block = block 19 | 20 | super.init(timeout: timeout) 21 | } 22 | 23 | override open func main() { 24 | block({ (output) in 25 | if let o = output { 26 | self.finish(with: o) 27 | } else { 28 | self.finish() 29 | } 30 | }) 31 | } 32 | } 33 | 34 | public typealias AsyncBlockResultOperation = AsyncBlockProducerOperation> 35 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncConsumerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncConsumerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-13. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An asynchronous variant of ConsumerOperation 12 | open class AsyncConsumerOperation: ConsumerOperation { 13 | override open var isAsynchronous: Bool { 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncConsumerProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncConsumerProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-13. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An asynchronous variant of ConsumerProducerOperation 12 | open class AsyncConsumerProducerOperation: ConsumerProducerOperation { 13 | override open var isAsynchronous: Bool { 14 | return true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-01-31. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class AsyncOperation : BaseOperation { 12 | override open var isAsynchronous: Bool { 13 | return true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/OperationPlus/AsyncProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-01-31. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class AsyncProducerOperation : ProducerOperation { 12 | override open var isAsynchronous: Bool { 13 | return true 14 | } 15 | } 16 | 17 | public typealias AsyncResultOperation = AsyncProducerOperation> 18 | -------------------------------------------------------------------------------- /Sources/OperationPlus/BaseOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2018-03-28. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class BaseOperation : Operation { 12 | public enum State { 13 | case notStarted 14 | case running 15 | case finished 16 | } 17 | 18 | private let lock: NSRecursiveLock 19 | private var state: State = .notStarted 20 | public let timeoutInterval: TimeInterval 21 | private var hasTimedOut: Bool = false 22 | 23 | public init(timeout: TimeInterval = .greatestFiniteMagnitude) { 24 | self.lock = NSRecursiveLock() 25 | self.timeoutInterval = timeout 26 | 27 | lock.name = "com.chimehq.Operation-Lock" 28 | 29 | super.init() 30 | } 31 | 32 | open func handleError(_ error: BaseOperationError) { 33 | fatalError(error.localizedDescription) 34 | } 35 | 36 | override open var isExecuting: Bool { 37 | lock.lock() 38 | defer { lock.unlock() } 39 | 40 | return state == .running 41 | } 42 | 43 | override open var isFinished: Bool { 44 | lock.lock() 45 | defer { lock.unlock() } 46 | 47 | return state == .finished 48 | } 49 | 50 | open var isTimedOut: Bool { 51 | lock.lock() 52 | defer { lock.unlock() } 53 | 54 | return hasTimedOut 55 | } 56 | 57 | private func markTimedOut() { 58 | lock.lock() 59 | defer { lock.unlock() } 60 | 61 | if isFinished { 62 | return 63 | } 64 | 65 | hasTimedOut = true 66 | timedOut() 67 | } 68 | 69 | /// Called when an operation times out. 70 | /// 71 | /// This is a useful hook for subclassers. By default, this will call cancel() and then finish(). If you 72 | /// do override this method, be sure to either call super or explicitly finish the operation. 73 | open func timedOut() { 74 | cancel() 75 | finish() 76 | } 77 | 78 | func beginExecution() { 79 | transition(to: .running) 80 | 81 | setupTimeout() 82 | } 83 | 84 | override open func start() { 85 | beginExecution() 86 | 87 | if checkForCancellation() { 88 | return 89 | } 90 | 91 | main() 92 | } 93 | 94 | override open func main() { 95 | // only really makes sense when subclassers override this 96 | finish() 97 | } 98 | 99 | override open func addDependency(_ op: Operation) { 100 | lock.lock() 101 | let invalid = state != .notStarted 102 | lock.unlock() 103 | 104 | if invalid { 105 | handleError(.dependencyAddedInInvalidState(op)) 106 | } 107 | 108 | super.addDependency(op) 109 | } 110 | } 111 | 112 | extension BaseOperation { 113 | private var deadlineTime: DispatchTime { 114 | return DispatchTime.now() + .milliseconds(Int(timeoutInterval * 1000.0)) 115 | } 116 | 117 | private func setupTimeout() { 118 | guard timeoutInterval < TimeInterval.greatestFiniteMagnitude else { 119 | return 120 | } 121 | 122 | guard !isCancelled else { 123 | return 124 | } 125 | 126 | DispatchQueue.global().asyncAfter(deadline: deadlineTime) { [weak self] in 127 | self?.markTimedOut() 128 | } 129 | } 130 | } 131 | 132 | extension BaseOperation { 133 | public func finish() { 134 | lock.lock() 135 | defer { lock.unlock() } 136 | 137 | // If we've timed out, it's still likely that finish 138 | // is going to be called again. We should be ok with that. 139 | if isTimedOut && isFinished { 140 | return 141 | } 142 | 143 | transition(to: .finished) 144 | } 145 | 146 | public func checkForCancellation() -> Bool { 147 | if isCancelled { 148 | finish() 149 | return true 150 | } 151 | 152 | return false 153 | } 154 | 155 | func transition(to newState: State) { 156 | lock.lock() 157 | defer { lock.unlock() } 158 | 159 | switch (state, newState) { 160 | case (.notStarted, .running): 161 | willChangeValue(forKey: "isExecuting") 162 | state = newState; 163 | didChangeValue(forKey: "isExecuting") 164 | case (.running, .finished): 165 | willChangeValue(forKey: "isExecuting") 166 | willChangeValue(forKey: "isFinished") 167 | state = newState; 168 | didChangeValue(forKey: "isFinished") 169 | didChangeValue(forKey: "isExecuting") 170 | case (_, .notStarted), (.finished, _), (.running, _), (.notStarted, _): 171 | handleError(.stateTransitionInvalid(newState)) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Sources/OperationPlus/BaseOperationError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseOperationError.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-02-03. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// The possible error states for a BaseOperation 13 | /// 14 | /// - dependencyAddedInInvalidState: The dependeny relationship cannot be 15 | /// enforced at this point in the operation's lifecycle 16 | /// - stateTransitionInvalid: A transition has been triggered that does not 17 | /// make sense. This represents a programming error. 18 | public enum BaseOperationError: Error { 19 | case dependencyAddedInInvalidState(Operation) 20 | case stateTransitionInvalid(BaseOperation.State) 21 | case timedOut 22 | } 23 | 24 | extension BaseOperationError: Equatable { 25 | } 26 | -------------------------------------------------------------------------------- /Sources/OperationPlus/BlockConsumerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockConsumerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2020-03-23. 6 | // Copyright © 2020 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A block-based version of ConsumerOperation 12 | public class BlockConsumerOperation: ConsumerOperation { 13 | private let block: (Input) -> Void 14 | 15 | public init(producerOp: ProducerOperation, timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping (Input) -> Void) { 16 | self.block = block 17 | 18 | super.init(producerOp: producerOp, timeout: timeout) 19 | } 20 | 21 | override open func main(with producedValue: Input) { 22 | block(producedValue) 23 | 24 | self.finish() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OperationPlus/BlockConsumerProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockConsumerProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-14. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A block-based version of ConsumerProducerOperation 12 | public class BlockConsumerProducerOperation: ConsumerProducerOperation { 13 | public typealias Block = (Input) -> Output? 14 | 15 | private let block: Block 16 | 17 | public init(producerOp: ProducerOperation, timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping Block) { 18 | self.block = block 19 | 20 | super.init(producerOp: producerOp, timeout: timeout) 21 | } 22 | 23 | override open func main(with producedValue: Input) { 24 | if let o = block(producedValue) { 25 | self.finish(with: o) 26 | } else { 27 | self.finish() 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/OperationPlus/BlockProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlockProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-13. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A block-based version of ProducerOperation 12 | public class BlockProducerOperation: ProducerOperation { 13 | private let block: () -> Output? 14 | 15 | public init(timeout: TimeInterval = .greatestFiniteMagnitude, block: @escaping () -> Output?) { 16 | self.block = block 17 | 18 | super.init(timeout: timeout) 19 | } 20 | 21 | override open func main() { 22 | if let o = block() { 23 | self.finish(with: o) 24 | } else { 25 | self.finish() 26 | } 27 | } 28 | } 29 | 30 | public typealias BlockResultOperation = BlockProducerOperation> 31 | -------------------------------------------------------------------------------- /Sources/OperationPlus/ConsumerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsumerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-09-13. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An operation that depends on and uses the value of a ProducerOperation 12 | public typealias ConsumerOperation = ConsumerProducerOperation 13 | -------------------------------------------------------------------------------- /Sources/OperationPlus/ConsumerProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConsumerProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-02-02. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An operation that depends on and uses the value of a ProducerOperation, and also 12 | /// itself produces a value 13 | open class ConsumerProducerOperation: ProducerOperation { 14 | private let producerOp: ProducerOperation 15 | 16 | /// Initializes the operation 17 | /// 18 | /// - Parameters: 19 | /// - producerOp: this will become a dependency of the instance 20 | /// - timeout: an optional timeout that will automatically finish the operation 21 | public init(producerOp: ProducerOperation, timeout: TimeInterval = .greatestFiniteMagnitude) { 22 | self.producerOp = producerOp 23 | 24 | super.init(timeout: timeout) 25 | 26 | addDependency(producerOp) 27 | } 28 | 29 | /// The value of the ProducerOperation 30 | /// 31 | /// This value can be null regardless of the generic type of ProducerOperation. This 32 | /// is because that operation might never complete, never start, or could 33 | /// timeout. 34 | public var producerValue: Input? { 35 | return producerOp.value 36 | } 37 | 38 | open override func main() { 39 | guard let producedValue = producerValue else { 40 | finish() 41 | return 42 | } 43 | 44 | main(with: producedValue) 45 | } 46 | 47 | /// A main entry point with a non-optional value 48 | /// 49 | /// This method is only invoked if the ProducerOperation 50 | /// dependency successfully produces a non-nil value. Otherwise, 51 | /// this operation will call finish() directly. 52 | /// 53 | /// This behavior is particularly useful for short-circuiting 54 | /// downstream work that doesn't need to happen if a dependency fails 55 | /// to produce a value. 56 | /// 57 | /// - Parameter producedValue: The ProducerOperation's value, if non-optional 58 | open func main(with producedValue: Input) { 59 | finish() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/Operation+Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Operation+Helpers.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 1/9/18. 6 | // Copyright © 2018 Chime Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension Operation { 12 | public func addDependencies(_ dependencies: [Operation]) { 13 | for op in dependencies { 14 | addDependency(op) 15 | } 16 | } 17 | 18 | public func addDependencies(_ dependencies: Set) { 19 | for op in dependencies { 20 | addDependency(op) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/OperationQueue+Creation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Creation.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-29. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension OperationQueue { 12 | public convenience init(name: String, maxConcurrentOperations count: Int = OperationQueue.defaultMaxConcurrentOperationCount) { 13 | self.init() 14 | 15 | self.name = name 16 | self.maxConcurrentOperationCount = count 17 | } 18 | } 19 | 20 | extension OperationQueue { 21 | public static func serialQueue() -> OperationQueue { 22 | let queue = OperationQueue() 23 | 24 | queue.maxConcurrentOperationCount = 1 25 | 26 | return queue 27 | } 28 | 29 | public static func serialQueue(named name: String) -> OperationQueue { 30 | return OperationQueue(name: name, maxConcurrentOperations: 1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/OperationQueue+Dependencies.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Dependencies.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-29. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension OperationQueue { 12 | public func addOperation(_ operation: Operation, dependency: Operation) { 13 | operation.addDependency(dependency) 14 | 15 | addOperation(operation) 16 | } 17 | 18 | public func addOperation(_ operation: Operation, dependencies: [Operation]) { 19 | operation.addDependencies(dependencies) 20 | 21 | addOperation(operation) 22 | } 23 | 24 | public func addOperation(_ operation: Operation, dependencies: Set) { 25 | operation.addDependencies(dependencies) 26 | 27 | addOperation(operation) 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/OperationQueue+Emptied.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Emptied.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-30. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension OperationQueue { 12 | @discardableResult 13 | public func currentOperationsFinished(completionBlock: @escaping () -> Void) -> Operation { 14 | let op = BlockOperation(block: completionBlock) 15 | 16 | addOperation(op, dependencies: self.operations) 17 | 18 | return op 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/OperationQueue+Enqueuing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Enqueuing.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-02-02. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension OperationQueue { 12 | 13 | /// Adds the specified operations to the queue. 14 | /// 15 | /// This method is just a wrapper for calling `addOperations(ops, waitUntilFinished: false)` 16 | /// 17 | /// - Parameter ops: The operations to be added to the queue. 18 | public func addOperations(_ ops: [Operation]) { 19 | addOperations(ops, waitUntilFinished: false) 20 | } 21 | 22 | /// Adds the specified operation to the queue. 23 | /// 24 | /// This method is a convenience wrapper around `AsyncBlockOperation`. Note that 25 | /// the completion block **must** be invoked when the async work has been completed. 26 | /// 27 | /// - Warning: Failure to invoke the completion block will prevent the queue from 28 | /// processing more operations. 29 | /// 30 | /// - Parameter block: The async operation to be added to the queue. 31 | public func addAsyncOperation(block: @escaping AsyncBlockOperation.CompletionHandler) { 32 | addOperation(AsyncBlockOperation(block: block)) 33 | } 34 | } 35 | 36 | extension OperationQueue { 37 | /// Adds the specified operation to the queue after a delay. 38 | /// 39 | /// This method schedules an `addOperation` call after the specified delay. 40 | /// 41 | /// - Parameter op: The operation to be added to the queue. 42 | /// - Parameter delay: The amount of time to wait before scheduling op. 43 | public func addOperation(_ op: Operation, afterDelay delay: TimeInterval) { 44 | let deadlineTime = DispatchTime.now() + delay 45 | 46 | DispatchQueue.global().asyncAfter(deadline: deadlineTime) { 47 | self.addOperation(op) 48 | } 49 | } 50 | 51 | /// Invokes the block on the queue after a delay. 52 | /// 53 | /// This method schedules an `addOperation` call after the specified delay. 54 | /// 55 | /// - Parameter delay: The amount of time to wait before scheduling op. 56 | /// - Parameter block: The block to be invoked on the queue. 57 | public func addOperation(afterDelay delay: TimeInterval, block: @escaping () -> Void) { 58 | addOperation(BlockOperation(block: block), afterDelay: delay) 59 | } 60 | } 61 | 62 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 63 | extension OperationQueue { 64 | /// Adds the specified async operation to the queue. 65 | /// 66 | /// It's worth careful considering the priority used for operations excuted this 67 | /// way. `OperationQueue` and `Task` may not cooperate nicely, and prioity inversion 68 | /// could be possible. 69 | /// 70 | /// - Parameter block: The async operation to be added to the queue. 71 | public func addOperation(block: @Sendable @escaping () async -> Void) { 72 | let op = AsyncBlockOperation { opBlock in 73 | Task.detached { 74 | await block() 75 | 76 | opBlock() 77 | } 78 | } 79 | 80 | addOperation(op) 81 | } 82 | 83 | /// Adds the specified async operation to the queue and returns its result. 84 | /// 85 | /// This function behaves just like the async version of addOperation, but can 86 | /// return a result value. 87 | /// 88 | /// - Parameter block: The async operation to be added to the queue. 89 | public func addResultOperation(block: @Sendable @escaping () async throws -> Success) async throws -> Success { 90 | return try await withCheckedThrowingContinuation({ continuation in 91 | addOperation { 92 | do { 93 | let value = try await block() 94 | 95 | continuation.resume(returning: value) 96 | } catch { 97 | continuation.resume(throwing: error) 98 | } 99 | } 100 | }) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/OperationQueue+Preconditions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueue+Helpers.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 11/18/17. 6 | // Copyright © 2017 Chime Software. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension OperationQueue { 12 | public static func preconditionMain() { 13 | if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) { 14 | dispatchPrecondition(condition: .onQueue(DispatchQueue.main)) 15 | } 16 | } 17 | 18 | public static func preconditionNotMain() { 19 | if #available(OSX 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) { 20 | dispatchPrecondition(condition: .notOnQueue(DispatchQueue.main)) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/OperationPlus/Extensions/Publishers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if canImport(Combine) 4 | import Combine 5 | 6 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 6.0, *) 7 | public extension Operation { 8 | /// A Combine publisher that produces one event when the operation finishes 9 | var publisher: AnyPublisher { 10 | return Future { promise in 11 | self.completionBlock = { 12 | promise(.success(())) 13 | } 14 | }.eraseToAnyPublisher() 15 | } 16 | } 17 | 18 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 6.0, *) 19 | public extension ProducerOperation { 20 | /// A Combine publisher that produces one event with the operation's result when it finishes 21 | var outputPublisher: AnyPublisher { 22 | return Future { promise in 23 | self.outputCompletionBlock = { value in 24 | promise(.success(value)) 25 | } 26 | }.eraseToAnyPublisher() 27 | } 28 | } 29 | 30 | /// A `ProducerOperation` subclass takes a publisher. When executed, it creates a subscription and outputs the results. 31 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 6.0, *) 32 | public class PublisherOperation: AsyncProducerOperation> { 33 | public typealias Publisher = AnyPublisher 34 | 35 | private var cancellable: AnyCancellable? 36 | private let chain: Publisher 37 | 38 | public init(publisher: Publisher, timeout: TimeInterval = .greatestFiniteMagnitude) { 39 | self.chain = publisher 40 | 41 | super.init(timeout: timeout) 42 | } 43 | 44 | public override func main() { 45 | self.cancellable = chain.sink(receiveCompletion: { [weak self] completion in 46 | switch completion { 47 | case .failure(let error): 48 | self?.finish(with: .failure(error)) 49 | case .finished: 50 | if self?.isFinished == false { 51 | self?.finish() 52 | } 53 | } 54 | }, receiveValue: { [weak self] output in 55 | self?.finish(with: .success(output)) 56 | }) 57 | } 58 | 59 | override public func cancel() { 60 | self.cancellable = nil 61 | super.cancel() 62 | } 63 | } 64 | 65 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 6.0, *) 66 | public extension Publisher { 67 | /// Creates an Operation which subscribes to the publisher when executed 68 | func operation() -> PublisherOperation { 69 | return PublisherOperation(publisher: self.eraseToAnyPublisher()) 70 | } 71 | 72 | /// Subscribes and runs a publisher on a queue, producing 73 | /// a new publisher with the results of that operation 74 | /// 75 | /// Using `receive(on:)` or `subscribe(on:)` with the same queue 76 | /// argument **before** this call in a chain will cause a deadlock 77 | /// if that queue's maxConcurrentOperations count is one. 78 | func execute(on queue: OperationQueue) -> AnyPublisher { 79 | let op = operation() 80 | 81 | queue.addOperation(op) 82 | 83 | return Future { promise in 84 | op.outputCompletionBlock = promise 85 | }.eraseToAnyPublisher() 86 | } 87 | } 88 | 89 | #endif 90 | -------------------------------------------------------------------------------- /Sources/OperationPlus/OperationPlus.docc/OperationPlus.md: -------------------------------------------------------------------------------- 1 | # ``OperationPlus`` 2 | 3 | - ``BaseOperation`` 4 | 5 | -------------------------------------------------------------------------------- /Sources/OperationPlus/ProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProducerOperation.swift 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-01-31. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | open class ProducerOperation : BaseOperation { 12 | public enum OutputCompletionBlockBehavior { 13 | case onCompletionOnly 14 | case onTimeOut(Output) 15 | } 16 | 17 | public typealias OutputHandler = (Output) -> Void 18 | 19 | public var outputCompletionBlock: OutputHandler? 20 | public var value: Output? 21 | public var outputCompletionBlockBehavior = OutputCompletionBlockBehavior.onCompletionOnly 22 | 23 | /// Block to return a cached value 24 | /// 25 | /// This block is run before main executed. If it 26 | /// returns a non-nil value, that value is used instead 27 | /// of executing the main body. This is useful for caching 28 | /// the result in external storage. 29 | public var readCacheBlock: (() -> Output?)? 30 | 31 | /// Block to write back a cached value 32 | /// 33 | /// This block is run after the operation has successfully 34 | /// produced a value. It can be used to write the value 35 | /// out to external storage. 36 | public var writeCacheBlock: ((Output) -> Void)? 37 | 38 | public func finish(with v: Output) { 39 | self.value = v 40 | writeCacheBlock?(v) 41 | 42 | switch (outputCompletionBlockBehavior, isCancelled, isTimedOut) { 43 | case (_, false, false): 44 | outputCompletionBlock?(v) 45 | case (_, _, true): 46 | return // do not call finish in this case 47 | default: 48 | break 49 | } 50 | 51 | finish() 52 | } 53 | 54 | override open func start() { 55 | beginExecution() 56 | 57 | if checkForCancellation() { 58 | return 59 | } 60 | 61 | if let v = readCacheBlock?() { 62 | self.finish(with: v) 63 | return 64 | } 65 | 66 | main() 67 | } 68 | 69 | override open func timedOut() { 70 | super.timedOut() 71 | 72 | switch (outputCompletionBlockBehavior, isCancelled, isTimedOut) { 73 | case (.onTimeOut(let v), _, true): 74 | self.value = v 75 | outputCompletionBlock?(v) 76 | default: 77 | break 78 | } 79 | } 80 | } 81 | 82 | public typealias ResultOperation = ProducerOperation> 83 | -------------------------------------------------------------------------------- /Sources/OperationTestingPlus/FulfillExpectationOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FulfillExpectationOperation.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-29. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | 11 | /// This Operation subclass will fulfill an `XCTestExpectation` when it 12 | /// completes. 13 | public class FulfillExpectationOperation: Operation { 14 | private let expectation: XCTestExpectation 15 | 16 | /// Initializer 17 | /// 18 | /// - Parameter expectation: will call `fulfill` on this instance when run 19 | public init(expectation: XCTestExpectation) { 20 | self.expectation = expectation 21 | } 22 | 23 | public override func main() { 24 | expectation.fulfill() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/OperationTestingPlus/NeverFinishingOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeverFinishingOperation.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-30. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OperationPlus 11 | 12 | /// A simple `Operation` that will never complete. Useful for testing 13 | /// timeout behavior of other systems. 14 | /// 15 | /// Warning: This operation will completely block a serial queue forever 16 | public class NeverFinishingOperation: BaseOperation { 17 | public override func main() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/OperationTestingPlus/NeverFinishingProducerOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NeverFinishingProducerOperation.swift 3 | // OperationTestingPlus 4 | // 5 | // Created by Matt Massicotte on 2020-01-15. 6 | // Copyright © 2020 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OperationPlus 11 | 12 | /// A simple `ProducerOperation` that will never complete. Useful for testing 13 | /// timeout behavior of other systems. 14 | /// 15 | /// Warning: This operation will completely block a serial queue forever 16 | public class NeverFinishingProducerOperation: ProducerOperation { 17 | public override func main() { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/OperationTestingPlus/OperationExpectation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationExpectation.swift 3 | // Operation 4 | // 5 | // Created by Matt Massicotte on 2019-01-29. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import class XCTest.XCTestExpectation 11 | 12 | /// An expectation that is fulfilled when an operation 13 | /// has finished execution. 14 | public class OperationExpectation: XCTestExpectation { 15 | public var queue: OperationQueue 16 | 17 | /// Initializes the expectation with an operation and an optional queue. 18 | /// 19 | /// - Parameters: 20 | /// - operation: This expectation is fulfilled when this operation completes 21 | /// - queue: The queue to use for the operation (optional) 22 | public convenience init(operation: Operation, queue: OperationQueue = OperationQueue()) { 23 | self.init(operations: [operation], queue: queue) 24 | } 25 | 26 | /// Initializes the expectation with an array of operations and an optional queue. 27 | /// 28 | /// - Parameters: 29 | /// - operations: This expectation is fulfilled when all operations complete 30 | /// - queue: The queue to use for the operation (optional) 31 | public init(operations: [Operation], queue: OperationQueue = OperationQueue()) { 32 | self.queue = queue 33 | 34 | super.init(description: "Operation Expectation") 35 | 36 | let expectationOp = FulfillExpectationOperation(expectation: self) 37 | expectationOp.addDependencies(operations) 38 | 39 | queue.addOperations(operations + [expectationOp]) 40 | } 41 | 42 | deinit { 43 | queue.cancelAllOperations() 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/AsyncBlockOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AsyncBlockOperationTests.swift 3 | // OperationPlusTests 4 | // 5 | // Created by Matt Massicotte on 2019-02-03. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import OperationTestingPlus 11 | @testable import OperationPlus 12 | 13 | class AsyncBlockOperationTests: XCTestCase { 14 | func testCallingCompletionBlock() { 15 | let op = AsyncBlockOperation { (completionBlock) in 16 | DispatchQueue.global().async { 17 | completionBlock() 18 | } 19 | } 20 | 21 | XCTAssertTrue(op.isAsynchronous) 22 | 23 | let expectation = OperationExpectation(operation: op) 24 | 25 | wait(for: [expectation], timeout: 1.0) 26 | 27 | XCTAssertTrue(op.isFinished) 28 | } 29 | 30 | func testNeverCallingCompletionBlock() { 31 | let op = AsyncBlockOperation { (completionBlock) in 32 | } 33 | 34 | let expectation = OperationExpectation(operation: op) 35 | expectation.isInverted = true 36 | 37 | wait(for: [expectation], timeout: 0.1) 38 | 39 | XCTAssertTrue(op.isReady) 40 | XCTAssertFalse(op.isFinished) 41 | XCTAssertFalse(op.isCancelled) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/BaseOperationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BaseOperationTests.swift 3 | // OperationTests 4 | // 5 | // Created by Matt Massicotte on 2018-11-12. 6 | // Copyright © 2018 Chime Systems. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import OperationTestingPlus 11 | @testable import OperationPlus 12 | 13 | class MockOperation: BaseOperation { 14 | typealias ExecutionHandler = (MockOperation) -> Void 15 | 16 | var handledError: BaseOperationError? 17 | var executionHandler: ExecutionHandler 18 | 19 | init(timeout: TimeInterval = .greatestFiniteMagnitude, executeBlock: ExecutionHandler? = nil) { 20 | self.executionHandler = executeBlock ?? { op in 21 | op.finish() 22 | } 23 | 24 | super.init(timeout: timeout) 25 | } 26 | 27 | override func handleError(_ error: BaseOperationError) { 28 | handledError = error 29 | } 30 | 31 | override func main() { 32 | executionHandler(self) 33 | } 34 | } 35 | 36 | class BaseOperationTests: XCTestCase { 37 | func testCompletion() { 38 | let op = BaseOperation() 39 | 40 | let expectation = OperationExpectation(operation: op) 41 | 42 | wait(for: [expectation], timeout: 0.1) 43 | 44 | XCTAssertFalse(op.isTimedOut) 45 | XCTAssertTrue(op.isFinished) 46 | XCTAssertFalse(op.isCancelled) 47 | } 48 | 49 | func testTimeOut() { 50 | let op = NeverFinishingOperation(timeout: 0.1) 51 | 52 | let expectation = OperationExpectation(operation: op) 53 | 54 | wait(for: [expectation], timeout: op.timeoutInterval * 2.0) 55 | 56 | XCTAssertTrue(op.isTimedOut) 57 | XCTAssertTrue(op.isFinished) 58 | XCTAssertTrue(op.isCancelled) 59 | } 60 | 61 | func testNeverFinishingOperation() { 62 | let op = NeverFinishingOperation() 63 | 64 | let expectation = OperationExpectation(operation: op) 65 | expectation.isInverted = true 66 | 67 | wait(for: [expectation], timeout: 0.1) 68 | 69 | XCTAssertTrue(op.isReady) 70 | XCTAssertFalse(op.isFinished) 71 | XCTAssertFalse(op.isCancelled) 72 | } 73 | 74 | func testAddDependencyAfterFinished() { 75 | let op = MockOperation() 76 | 77 | let expectation = OperationExpectation(operation: op) 78 | 79 | wait(for: [expectation], timeout: 1.0) 80 | 81 | XCTAssertTrue(op.isFinished) 82 | 83 | op.addDependency(Operation()) 84 | 85 | guard case .dependencyAddedInInvalidState? = op.handledError else { 86 | XCTFail("Incorrect error") 87 | return 88 | } 89 | } 90 | 91 | func testKVONotifications() { 92 | let op = MockOperation() 93 | 94 | let executingKVOExpectation = XCTKVOExpectation(keyPath: "isExecuting", object: op, expectedValue: true) 95 | let finishedKVOExpectation = XCTKVOExpectation(keyPath: "isFinished", object: op, expectedValue: true) 96 | 97 | let expectations = [ 98 | executingKVOExpectation, 99 | finishedKVOExpectation, 100 | ] 101 | 102 | // I couldn't figure out a way to correctly synchronize isFinished with 103 | // detecting completion using an OperationExpectation. So, I just 104 | // seperated out the two waits. Not ideal, but also not flakey. 105 | let completionExpectation = OperationExpectation(operation: op) 106 | 107 | wait(for: expectations, timeout: 1.0, enforceOrder: true) 108 | wait(for: [completionExpectation], timeout: 1.0) 109 | 110 | XCTAssertTrue(op.isFinished) 111 | } 112 | } 113 | 114 | extension BaseOperationTests { 115 | func testFinishBeforeStartingIsInvalid() { 116 | let op = MockOperation() 117 | 118 | XCTAssertNil(op.handledError) 119 | 120 | op.finish() 121 | 122 | XCTAssertEqual(op.handledError, BaseOperationError.stateTransitionInvalid(.finished)) 123 | } 124 | 125 | func testTimeoutBeforeStartingIsInvalid() { 126 | let op = MockOperation() 127 | 128 | XCTAssertNil(op.handledError) 129 | 130 | op.timedOut() 131 | 132 | XCTAssertEqual(op.handledError, BaseOperationError.stateTransitionInvalid(.finished)) 133 | } 134 | 135 | func testDoubleFinishIsInvalid() { 136 | let op = MockOperation(executeBlock: { (o) in 137 | o.finish() 138 | o.finish() 139 | }) 140 | 141 | let expectation = OperationExpectation(operation: op) 142 | 143 | wait(for: [expectation], timeout: 1.0) 144 | 145 | XCTAssertEqual(op.handledError, BaseOperationError.stateTransitionInvalid(.finished)) 146 | } 147 | 148 | func testFinishAfterTimeoutIsValid() { 149 | let secondCompletionExpectation = expectation(description: "second completion") 150 | 151 | let op = MockOperation(timeout: 0.1, executeBlock: { (o) in 152 | usleep(UInt32(o.timeoutInterval * 1000.0 * 1000.0 * 2.0)) 153 | o.finish() 154 | secondCompletionExpectation.fulfill() 155 | }) 156 | 157 | let expectation = OperationExpectation(operation: op) 158 | 159 | wait(for: [expectation], timeout: 1.0) 160 | 161 | XCTAssertNil(op.handledError) 162 | XCTAssertTrue(op.isTimedOut) 163 | XCTAssertTrue(op.isFinished) 164 | 165 | wait(for: [secondCompletionExpectation], timeout: 1.0) 166 | 167 | XCTAssertNil(op.handledError) 168 | XCTAssertTrue(op.isTimedOut) 169 | XCTAssertTrue(op.isFinished) 170 | } 171 | 172 | func testCancelBeforeStarting() { 173 | let op = MockOperation(executeBlock: { (o) in 174 | o.finish() 175 | }) 176 | 177 | op.cancel() 178 | 179 | let expectation = OperationExpectation(operation: op) 180 | 181 | wait(for: [expectation], timeout: 0.1) 182 | 183 | XCTAssertNil(op.handledError) 184 | XCTAssertTrue(op.isCancelled) 185 | XCTAssertTrue(op.isFinished) 186 | } 187 | 188 | func testFinishAfterCancellationDuringExecutionIsValid() { 189 | let op = MockOperation(executeBlock: { (o) in 190 | o.cancel() 191 | o.finish() 192 | }) 193 | 194 | let expectation = OperationExpectation(operation: op) 195 | 196 | wait(for: [expectation], timeout: 0.1) 197 | 198 | XCTAssertNil(op.handledError) 199 | XCTAssertTrue(op.isCancelled) 200 | XCTAssertTrue(op.isFinished) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/CombineTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CombineTests.swift 3 | // 4 | // 5 | // Created by Matthew Massicotte on 2022-02-25. 6 | // 7 | 8 | import XCTest 9 | import OperationTestingPlus 10 | @testable import OperationPlus 11 | 12 | #if canImport(Combine) 13 | import Combine 14 | 15 | @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 7.0, *) 16 | class CombineTests: XCTestCase { 17 | var subs = Set() 18 | 19 | func testOperationPublisher() { 20 | let op = Operation() 21 | 22 | let valueExpectation = expectation(description: "Got Value") 23 | 24 | op.publisher 25 | .sink { 26 | valueExpectation.fulfill() 27 | } 28 | .store(in: &subs) 29 | 30 | let opExpectation = OperationExpectation(operations: [op]) 31 | 32 | wait(for: [valueExpectation, opExpectation], timeout: 1.0) 33 | 34 | XCTAssertTrue(op.isFinished) 35 | } 36 | 37 | func testOutputPublisher() throws { 38 | let op = IntResultProducerOperation(.success(42)) 39 | 40 | let valueExpectation = expectation(description: "Got Value") 41 | valueExpectation.expectedFulfillmentCount = 2 42 | 43 | let pub = op 44 | .outputPublisher 45 | .flatMap { result in 46 | return result.publisher 47 | } 48 | 49 | // create two subscribers 50 | pub 51 | .sink { error in 52 | } receiveValue: { value in 53 | valueExpectation.fulfill() 54 | } 55 | .store(in: &subs) 56 | 57 | pub 58 | .sink { error in 59 | } receiveValue: { value in 60 | valueExpectation.fulfill() 61 | } 62 | .store(in: &subs) 63 | 64 | let opExpectation = OperationExpectation(operations: [op]) 65 | 66 | wait(for: [valueExpectation, opExpectation], timeout: 1.0, enforceOrder: true) 67 | 68 | XCTAssertTrue(op.isFinished) 69 | XCTAssertEqual(try op.resultValue.get(), 42) 70 | } 71 | 72 | func testPublisherOperation() throws { 73 | let pub = Just(1).eraseToAnyPublisher() 74 | 75 | let op = PublisherOperation(publisher: pub) 76 | 77 | let valueExpectation = expectation(description: "Got Value") 78 | 79 | op.outputCompletionBlock = { value in 80 | valueExpectation.fulfill() 81 | } 82 | 83 | let opExpectation = OperationExpectation(operations: [op]) 84 | 85 | wait(for: [valueExpectation, opExpectation], timeout: 1.0) 86 | 87 | XCTAssertTrue(op.isFinished) 88 | XCTAssertEqual(op.value, .success(1)) 89 | } 90 | 91 | func testOperationOperator() { 92 | let executionExpectation = expectation(description: "Execution") 93 | 94 | let op = Deferred { 95 | Future { block in 96 | DispatchQueue.global().async { 97 | executionExpectation.fulfill() 98 | block(.success(42)) 99 | } 100 | } 101 | }.operation() 102 | 103 | let opExpectation = OperationExpectation(operations: [op]) 104 | 105 | wait(for: [executionExpectation, opExpectation], timeout: 1.0) 106 | 107 | XCTAssertTrue(op.isFinished) 108 | XCTAssertEqual(op.value, .success(42)) 109 | } 110 | 111 | func testExecuteOnOperator() { 112 | let queue = OperationQueue.serialQueue() 113 | 114 | let startExp = expectation(description: "Starting") 115 | let completingExp = expectation(description: "Starting") 116 | 117 | let pub = Deferred { 118 | Future { block in 119 | startExp.fulfill() 120 | 121 | DispatchQueue.global().async { 122 | completingExp.fulfill() 123 | 124 | block(.success(42)) 125 | } 126 | } 127 | } 128 | .subscribe(on: DispatchQueue.global()) 129 | .execute(on: queue) 130 | 131 | // create two subsribers 132 | let firstSubExp = expectation(description: "First Value") 133 | pub 134 | .receive(on: DispatchQueue.global()) 135 | .sink { _ in 136 | firstSubExp.fulfill() 137 | } 138 | .store(in: &subs) 139 | 140 | let secondSubExp = expectation(description: "Second Value") 141 | pub 142 | .receive(on: DispatchQueue.global()) 143 | .sink { _ in 144 | secondSubExp.fulfill() 145 | } 146 | .store(in: &subs) 147 | 148 | let nextOpExp = expectation(description: "Next Op") 149 | 150 | let op = Operation() 151 | 152 | op.completionBlock = { 153 | nextOpExp.fulfill() 154 | } 155 | 156 | queue.addOperation(op) 157 | 158 | wait(for: [startExp, completingExp, nextOpExp], timeout: 1.0, enforceOrder: true) 159 | wait(for: [firstSubExp, secondSubExp], timeout: 1.0) 160 | } 161 | } 162 | 163 | #endif 164 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/OperationPlusTests.xcconfig: -------------------------------------------------------------------------------- 1 | // 2 | // OperationPlusTests.xcconfig 3 | // OperationPlus 4 | // 5 | // Created by Matt Massicotte on 2019-01-31. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | // Configuration settings file format documentation can be found at: 10 | // https://help.apple.com/xcode/#/dev745c5c974 11 | 12 | #include "xcconfigs/UniversalFramework_Test.xcconfig" 13 | 14 | PRODUCT_NAME = OperationPlusTests 15 | PRODUCT_BUNDLE_IDENTIFIER = com.chimehq.OperationPlusTests 16 | PRODUCT_MODULE_NAME = OperationPlusTests 17 | 18 | INFOPLIST_FILE = OperationPlusTests/Info.plist 19 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/OperationQueueTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // OperationQueueTests.swift 3 | // OperationTests 4 | // 5 | // Created by Matt Massicotte on 2019-01-30. 6 | // Copyright © 2019 Chime Systems. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import OperationTestingPlus 11 | @testable import OperationPlus 12 | 13 | class OperationQueueTests: XCTestCase { 14 | func testSerialCreation() { 15 | let queue = OperationQueue.serialQueue() 16 | 17 | XCTAssertEqual(queue.maxConcurrentOperationCount, 1) 18 | } 19 | 20 | func testNamedSerialCreation() { 21 | let queue = OperationQueue.serialQueue(named: "myqueue") 22 | 23 | XCTAssertEqual(queue.name, "myqueue") 24 | XCTAssertEqual(queue.maxConcurrentOperationCount, 1) 25 | } 26 | 27 | func testDependencies() { 28 | let opA = Operation() 29 | let opB = Operation() 30 | let queue = OperationQueue() 31 | 32 | queue.addOperation(opA, dependencies: [opB]) 33 | 34 | XCTAssertEqual(opA.dependencies, [opB]) 35 | } 36 | 37 | func testDependency() { 38 | let opA = Operation() 39 | let opB = Operation() 40 | let queue = OperationQueue() 41 | 42 | queue.addOperation(opA, dependency: opB) 43 | 44 | XCTAssertEqual(opA.dependencies, [opB]) 45 | } 46 | 47 | func testConvenienceInitializer() { 48 | let queue = OperationQueue(name: "MyQueue", maxConcurrentOperations: 10) 49 | 50 | XCTAssertEqual(queue.name, "MyQueue") 51 | XCTAssertEqual(queue.maxConcurrentOperationCount, 10) 52 | } 53 | 54 | func testConvenienceInitializerDefault() { 55 | let queue = OperationQueue(name: "MyQueue") 56 | 57 | XCTAssertEqual(queue.name, "MyQueue") 58 | XCTAssertEqual(queue.maxConcurrentOperationCount, OperationQueue.defaultMaxConcurrentOperationCount) 59 | } 60 | 61 | func testCurrentOperationsCompleteHandler() { 62 | let queue = OperationQueue() 63 | 64 | let opExpectation = OperationExpectation(operation: Operation(), queue: queue) 65 | 66 | let finishedExpectation = XCTestExpectation(description: "Queue finished block invoked") 67 | 68 | queue.currentOperationsFinished { 69 | finishedExpectation.fulfill() 70 | } 71 | 72 | wait(for: [opExpectation, finishedExpectation], timeout: 0.1, enforceOrder: true) 73 | } 74 | 75 | func testPreconditionMain() { 76 | OperationQueue.preconditionMain() 77 | } 78 | 79 | func testPreconditionNotMain() { 80 | let op = BlockOperation { 81 | OperationQueue.preconditionNotMain() 82 | } 83 | 84 | let expectation = OperationExpectation(operation: op) 85 | 86 | wait(for: [expectation], timeout: 1.0) 87 | } 88 | 89 | func testAddAsyncBlock() { 90 | let queue = OperationQueue.serialQueue() 91 | 92 | let blockExpecation = XCTestExpectation(description: "Block Expectation") 93 | 94 | queue.addAsyncOperation { (completionHandler) in 95 | blockExpecation.fulfill() 96 | completionHandler() 97 | } 98 | 99 | let nextOpExpectation = OperationExpectation(operation: Operation(), queue: queue) 100 | 101 | wait(for: [blockExpecation, nextOpExpectation], timeout: 1.0, enforceOrder: true) 102 | } 103 | 104 | func testAddOperationAfterDelay() { 105 | let queue = OperationQueue() 106 | let op = Operation() 107 | let dependentOp = Operation() 108 | dependentOp.addDependency(op) 109 | 110 | queue.addOperation(op, afterDelay: 0.1) 111 | 112 | let expectation = OperationExpectation(operation: dependentOp, queue: queue) 113 | 114 | wait(for: [expectation], timeout: 0.5) 115 | } 116 | 117 | func testAddDependencies() { 118 | let queue = OperationQueue() 119 | let opA = Operation() 120 | let opB = Operation() 121 | let opC = Operation() 122 | 123 | queue.addOperations([opA, opB]) 124 | queue.addOperation(opC, dependencies: [opB, opA]) 125 | 126 | XCTAssertEqual(opA.dependencies.count, 0) 127 | XCTAssertEqual(opB.dependencies.count, 0) 128 | XCTAssertEqual(opC.dependencies.count, 2) 129 | XCTAssertTrue(opC.dependencies.contains(opA)) 130 | XCTAssertTrue(opC.dependencies.contains(opB)) 131 | } 132 | 133 | func testAddSetDependencies() { 134 | let queue = OperationQueue() 135 | let opA = Operation() 136 | let opB = Operation() 137 | let opC = Operation() 138 | 139 | queue.addOperations([opA, opB]) 140 | queue.addOperation(opC, dependencies: Set([opB, opA])) 141 | 142 | XCTAssertEqual(opA.dependencies.count, 0) 143 | XCTAssertEqual(opB.dependencies.count, 0) 144 | XCTAssertEqual(opC.dependencies.count, 2) 145 | XCTAssertTrue(opC.dependencies.contains(opA)) 146 | XCTAssertTrue(opC.dependencies.contains(opB)) 147 | } 148 | 149 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 150 | func testAddOperationAsync() { 151 | let queue = OperationQueue() 152 | 153 | let expectation = XCTestExpectation() 154 | 155 | queue.addOperation { 156 | await Task { }.value 157 | 158 | expectation.fulfill() 159 | } 160 | 161 | wait(for: [expectation], timeout: 0.5) 162 | } 163 | 164 | @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) 165 | func testAddResultOperationAsync() async throws { 166 | let queue = OperationQueue() 167 | 168 | let value = try await queue.addResultOperation { 169 | return 5 170 | } 171 | 172 | XCTAssertEqual(value, 5) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/ProducerConsumerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ProducerConsumerTests.swift 3 | // OperationPlusTests 4 | // 5 | // Created by Matt Massicotte on 2019-02-02. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import OperationTestingPlus 11 | @testable import OperationPlus 12 | 13 | class ProducerConsumerTests: XCTestCase { 14 | func testProducerToConsumer() { 15 | let opA = IntProducerOperation(intValue: 10) 16 | let opB = IntConsumerOperation(producerOp: opA) 17 | 18 | let expectation = OperationExpectation(operations: [opA, opB]) 19 | 20 | wait(for: [expectation], timeout: 1.0) 21 | 22 | XCTAssertTrue(opA.isFinished) 23 | XCTAssertTrue(opB.isFinished) 24 | XCTAssertEqual(opB.producerValue, 10) 25 | } 26 | 27 | func testProducerToProducerConsumerToConsumer() { 28 | let opA = IntProducerOperation(intValue: 42) 29 | let opB = IntToBoolOperation(producerOp: opA) 30 | let opC = BoolConsumerOperation(producerOp: opB) 31 | 32 | let randomOrderedOps = [opA, opB, opC].shuffled() 33 | 34 | let expectation = OperationExpectation(operations: randomOrderedOps) 35 | 36 | wait(for: [expectation], timeout: 1.0) 37 | 38 | XCTAssertTrue(opA.isFinished) 39 | XCTAssertTrue(opB.isFinished) 40 | XCTAssertTrue(opC.isFinished) 41 | XCTAssertEqual(opC.producerValue, true) 42 | } 43 | 44 | func testFailedProducer() { 45 | let opA = IntProducerOperation(intValue: nil) 46 | let opB = IntConsumerOperation(producerOp: opA) 47 | 48 | let expectation = OperationExpectation(operations: [opA, opB]) 49 | 50 | wait(for: [expectation], timeout: 1.0) 51 | 52 | XCTAssertTrue(opA.isFinished) 53 | XCTAssertTrue(opB.isFinished) 54 | XCTAssertNil(opB.producerValue) 55 | } 56 | 57 | func testMainCorrectlyInvokesFinishWithoutValue() { 58 | let opA = IntProducerOperation(intValue: nil) 59 | let opB = IntToBoolOperation(producerOp: opA) 60 | let opC = BoolConsumerOperation(producerOp: opB) 61 | 62 | let randomOrderedOps = [opA, opB, opC].shuffled() 63 | 64 | let expectation = OperationExpectation(operations: randomOrderedOps) 65 | 66 | wait(for: [expectation], timeout: 1.0) 67 | 68 | XCTAssertTrue(opA.isFinished) 69 | XCTAssertNil(opA.value) 70 | XCTAssertTrue(opB.isFinished) 71 | XCTAssertNil(opB.producerValue) 72 | XCTAssertNil(opB.value) 73 | XCTAssertTrue(opC.isFinished) 74 | XCTAssertNil(opC.producerValue) 75 | } 76 | 77 | func testCallingAsyncCompletionCallback() { 78 | let op = AsyncBlockProducerOperation { (completionBlock) in 79 | DispatchQueue.global().async { 80 | completionBlock(42) 81 | } 82 | } 83 | 84 | XCTAssertNil(op.value) 85 | XCTAssertTrue(op.isAsynchronous) 86 | 87 | let expectation = OperationExpectation(operation: op) 88 | 89 | wait(for: [expectation], timeout: 1.0) 90 | 91 | XCTAssertTrue(op.isFinished) 92 | XCTAssertEqual(op.value, 42) 93 | } 94 | 95 | func testReturningValueFromBlockProducer() { 96 | let op = BlockProducerOperation { 97 | return 42 98 | } 99 | 100 | XCTAssertNil(op.value) 101 | 102 | let expectation = OperationExpectation(operation: op) 103 | 104 | wait(for: [expectation], timeout: 1.0) 105 | 106 | XCTAssertTrue(op.isFinished) 107 | XCTAssertEqual(op.value, 42) 108 | } 109 | 110 | func testReturningNilFromBlockProducer() { 111 | let op = BlockProducerOperation { 112 | return nil 113 | } 114 | 115 | XCTAssertNil(op.value) 116 | 117 | let expectation = OperationExpectation(operation: op) 118 | 119 | wait(for: [expectation], timeout: 1.0) 120 | 121 | XCTAssertTrue(op.isFinished) 122 | XCTAssertNil(op.value) 123 | } 124 | 125 | func testCachingFromProducerOperation() { 126 | let op = IntProducerOperation(intValue: 42) 127 | var writeValue = 0 128 | 129 | op.readCacheBlock = { 5 } 130 | op.writeCacheBlock = { writeValue = $0 } 131 | 132 | let expectation = OperationExpectation(operation: op) 133 | 134 | wait(for: [expectation], timeout: 1.0) 135 | 136 | XCTAssertTrue(op.isFinished) 137 | XCTAssertEqual(op.value, 5) 138 | XCTAssertEqual(writeValue, 5) 139 | } 140 | 141 | func testBlockConsumerProducerOperation() { 142 | let opA = IntProducerOperation(intValue: 10) 143 | let blockOp = BlockConsumerProducerOperation(producerOp: opA) { (producedValue) in 144 | return producedValue * 10 145 | } 146 | let opB = IntConsumerOperation(producerOp: blockOp) 147 | 148 | let expectation = OperationExpectation(operations: [opA, blockOp, opB]) 149 | 150 | wait(for: [expectation], timeout: 1.0) 151 | 152 | XCTAssertTrue(opA.isFinished) 153 | XCTAssertTrue(blockOp.isFinished) 154 | XCTAssertEqual(blockOp.producerValue, 10) 155 | XCTAssertTrue(opB.isFinished) 156 | XCTAssertEqual(opB.producerValue, 100) 157 | } 158 | 159 | func testProducerTimeOutValue() { 160 | let op = NeverFinishingProducerOperation(timeout: 0.1) 161 | op.outputCompletionBlockBehavior = .onTimeOut(10) 162 | 163 | let expectation = OperationExpectation(operation: op) 164 | 165 | wait(for: [expectation], timeout: op.timeoutInterval * 2.0) 166 | 167 | XCTAssertTrue(op.isTimedOut) 168 | XCTAssertTrue(op.isFinished) 169 | XCTAssertTrue(op.isCancelled) 170 | XCTAssertEqual(op.value, 10) 171 | } 172 | 173 | func testAsyncBlockConsumerOperation() { 174 | let opA = IntProducerOperation(intValue: 10) 175 | 176 | let valueExpectation = expectation(description: "Got Value") 177 | 178 | let blockOp = AsyncBlockConsumerOperation(producerOp: opA) { (value, opCompletionBlock) in 179 | DispatchQueue.global().async { 180 | if value == 10 { 181 | valueExpectation.fulfill() 182 | } 183 | opCompletionBlock() 184 | } 185 | } 186 | 187 | let opExpectation = OperationExpectation(operations: [opA, blockOp]) 188 | 189 | wait(for: [valueExpectation, opExpectation], timeout: 1.0) 190 | 191 | XCTAssertTrue(opA.isFinished) 192 | XCTAssertTrue(blockOp.isFinished) 193 | } 194 | 195 | func testBlockConsumerOperation() { 196 | let opA = IntProducerOperation(intValue: 10) 197 | 198 | let valueExpectation = expectation(description: "Got Value") 199 | 200 | let blockOp = BlockConsumerOperation(producerOp: opA) { (value) in 201 | if value == 10 { 202 | valueExpectation.fulfill() 203 | } 204 | } 205 | 206 | let opExpectation = OperationExpectation(operations: [opA, blockOp]) 207 | 208 | wait(for: [valueExpectation, opExpectation], timeout: 1.0) 209 | 210 | XCTAssertTrue(opA.isFinished) 211 | XCTAssertTrue(blockOp.isFinished) 212 | } 213 | 214 | func testAsyncBlockConsumerProducerOperation() { 215 | let opA = IntProducerOperation(intValue: 10) 216 | 217 | let blockOp = AsyncBlockConsumerProducerOperation(producerOp: opA) { (value, block) in 218 | let newValue = value * 10 219 | 220 | DispatchQueue.global().async { 221 | block(newValue) 222 | } 223 | } 224 | 225 | XCTAssertTrue(blockOp.isAsynchronous) 226 | 227 | let valueExpectation = expectation(description: "Got Value") 228 | 229 | let opB = BlockConsumerOperation(producerOp: blockOp) { (value) in 230 | if value == 100 { 231 | valueExpectation.fulfill() 232 | } 233 | } 234 | 235 | let opExpectation = OperationExpectation(operations: [opA, blockOp, opB]) 236 | 237 | wait(for: [valueExpectation, opExpectation], timeout: 1.0) 238 | 239 | XCTAssertTrue(opA.isFinished) 240 | XCTAssertTrue(blockOp.isFinished) 241 | XCTAssertTrue(opB.isFinished) 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /Tests/OperationPlusTests/TestHelpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestHelpers.swift 3 | // OperationPlusTests 4 | // 5 | // Created by Matt Massicotte on 2019-02-04. 6 | // Copyright © 2019 Chime Systems Inc. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import OperationPlus 11 | 12 | class IntProducerOperation: ProducerOperation { 13 | let intValue: Int? 14 | 15 | init(intValue: Int?) { 16 | self.intValue = intValue 17 | } 18 | 19 | override func main() { 20 | if let v = intValue { 21 | finish(with: v) 22 | } else { 23 | finish() 24 | } 25 | } 26 | } 27 | 28 | class IntToBoolOperation: ConsumerProducerOperation { 29 | override func main(with producedValue: Int) { 30 | finish(with: producedValue == 42) 31 | } 32 | } 33 | 34 | typealias BoolConsumerOperation = ConsumerOperation 35 | typealias IntConsumerOperation = ConsumerOperation 36 | 37 | class IntResultProducerOperation: ProducerOperation> { 38 | let resultValue: Result 39 | 40 | init(_ value: Result) { 41 | self.resultValue = value 42 | } 43 | 44 | override func main() { 45 | self.finish(with: resultValue) 46 | } 47 | } 48 | --------------------------------------------------------------------------------