├── .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 |
--------------------------------------------------------------------------------