├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── Package.swift
├── README.md
├── Sources
└── CombineAsyncable
│ ├── AnyCancellable+Cancel.swift
│ ├── Future+Task.swift
│ ├── Publisher+Task.swift
│ └── Sequence+Store.swift
└── Tests
├── CombineAsyncableTests
├── PublisherTests.swift
├── StubError.swift
└── XCTestManifests.swift
└── LinuxMain.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 hcrane
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "CombineAsyncable",
8 | products: [
9 | .library(
10 | name: "CombineAsyncable",
11 | targets: ["CombineAsyncable"]),
12 | ],
13 | dependencies: [],
14 | targets: [
15 | .target(
16 | name: "CombineAsyncable",
17 | dependencies: []),
18 | .testTarget(
19 | name: "CombineAsyncableTests",
20 | dependencies: ["CombineAsyncable"]),
21 | ]
22 | )
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CombineAsyncable
2 |
3 | ## Description
4 |
5 | It bridges from Combine to Concurrency.
6 |
7 | A small set of extensions that allow to combine new swift concurrency with Combine.
8 |
9 | [Here](https://qiita.com/hcrane/items/dd7d1cbe5a3d2acfe252) are the details in Japanese.
10 |
11 |
12 | ## Operator
13 |
14 | ### .asyncMap
15 |
16 | ```.swift
17 | Just(10)
18 | .asyncMap { number in
19 | await doSomething(number)
20 | }
21 | .sink { value in
22 | // handle value
23 | }
24 | .store(in: &cancellable)
25 | ```
26 |
27 |
28 | ### .asyncMapWithThrows
29 |
30 | ```.swift
31 | let subject = PassthroughSubject<(), Never>()
32 |
33 | subject
34 | .asyncMapWithThrows {
35 | try await APIClient.fetch()
36 | }
37 | .sink(receiveCompletion: { result in
38 | // handle result
39 | }, receiveValue: { value in
40 | // handle value
41 | })
42 | .store(in: &cancellable)
43 |
44 | subject.send(())
45 | ```
46 |
47 | ### .asyncSink
48 |
49 | ```.swift
50 | Just(10)
51 | .asyncSink { number in
52 | await doSomething(number)
53 | }
54 | .store(in: &cancellable)
55 | ```
56 |
57 | ### .asyncSinkWithThrows
58 |
59 | ```.swift
60 | let subject = PassthroughSubject<(), Never>()
61 |
62 | subject
63 | .setFailureType(to: Error.self)
64 | .asyncSinkWithThrows(receiveCompletion: { result in
65 | // handling result
66 | }, receiveValue: {
67 | let response = try await APIClient.fetch()
68 | // handling response
69 | })
70 | .store(in: &cancellable)
71 |
72 | subject.send(())
73 | ```
74 |
75 | ### Swift Package Manager
76 |
77 | Add the following dependency to your Package.swift file:
78 |
79 | ```
80 | .package(url: "https://github.com/crane-hiromu/CombineAsyncable", "0.3.1"..<"1.0.0")
81 | ```
82 |
83 | ### License
84 |
85 | MIT, of course ;-) See the LICENSE file.
86 |
--------------------------------------------------------------------------------
/Sources/CombineAsyncable/AnyCancellable+Cancel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Publisher+Task.swift
3 | //
4 | //
5 | // Created by h.tsuruta on 2022/10/06.
6 | //
7 |
8 | #if compiler(>=5.5)
9 | import Combine
10 |
11 | // MARK: - AnyCancellable Extension
12 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
13 | public extension AnyCancellable {
14 |
15 | func cancel(completion: @escaping () -> Void) -> AnyCancellable {
16 | .init { completion() }
17 | }
18 | }
19 |
20 | #endif
21 |
--------------------------------------------------------------------------------
/Sources/CombineAsyncable/Future+Task.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Future+Task.swift
3 | //
4 | //
5 | // Created by h.tsuruta on 2022/10/06.
6 | //
7 |
8 | #if compiler(>=5.5) && canImport(_Concurrency)
9 | import Combine
10 |
11 | // MARK: - Future Extension (Never)
12 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
13 | public extension Future where Failure == Never {
14 |
15 | /// Example
16 | ///
17 | /// func exec() -> Future<(), Never> {
18 | /// Future {
19 | /// await callAsyncFunction()
20 | /// }
21 | /// }
22 | ///
23 | convenience init(
24 | priority: TaskPriority? = nil,
25 | operation: @escaping () async -> Output
26 | ) {
27 | self.init { promise in
28 | Task(priority: priority) {
29 | try? Task.checkCancellation()
30 | let result = await operation()
31 | promise(.success(result))
32 | }
33 | }
34 | }
35 | }
36 |
37 | // MARK: - Future Extension (Error)
38 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
39 | public extension Future where Failure == Error {
40 |
41 | /// Example
42 | ///
43 | /// func exec() -> Future<(), Error> {
44 | /// Future {
45 | /// try await callAsyncThrowsFunction()
46 | /// }
47 | /// }
48 | ///
49 | convenience init(
50 | priority: TaskPriority? = nil,
51 | operation: @escaping () async throws -> Output
52 | ) {
53 | self.init { promise in
54 | Task(priority: priority) {
55 | try Task.checkCancellation()
56 | do {
57 | let result = try await operation()
58 | promise(.success(result))
59 | } catch {
60 | promise(.failure(error))
61 | }
62 | }
63 | }
64 | }
65 | }
66 |
67 | #endif
68 |
--------------------------------------------------------------------------------
/Sources/CombineAsyncable/Publisher+Task.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Publisher+Task.swift
3 | //
4 | //
5 | // Created by h.tsuruta on 2022/10/06.
6 | //
7 |
8 | #if compiler(>=5.5) && canImport(_Concurrency)
9 | import Combine
10 |
11 | // MARK: - Publisher Extension (Never)
12 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
13 | public extension Publisher where Self.Failure == Never {
14 |
15 | /// Example
16 | ///
17 | /// let subject = PassthroughSubject()
18 | ///
19 | /// var cancellable = subject.asyncSink { in
20 | /// await callAsyncFunction()
21 | /// }
22 | ///
23 | /// subject.send(())
24 | ///
25 | func asyncSink(
26 | priority: TaskPriority? = nil,
27 | receiveValue: @escaping ((Self.Output) async -> Void)
28 | ) -> Set {
29 | var set = Set()
30 | var task: Task?
31 |
32 | let cancellable = self.sink { value in
33 | task = Task(priority: priority) {
34 | guard !Task.isCancelled else { return }
35 | await receiveValue(value)
36 | }
37 | }
38 |
39 | // store cancellable to prevent from canceling stream
40 | cancellable
41 | .store(in: &set)
42 |
43 | // generate task cancellable
44 | cancellable
45 | .cancel { task?.cancel() }
46 | .store(in: &set)
47 |
48 | return set
49 | }
50 | }
51 |
52 | // MARK: - Publisher Extension (Error)
53 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
54 | public extension Publisher where Self.Failure == Error {
55 |
56 | /// Example
57 | ///
58 | /// Just(99)
59 | /// .setFailureType(to: Error.self)
60 | /// .asyncSinkWithThrows(receiveCompletion: { result in
61 | /// try await callAsyncThrowsFunction()
62 | /// }, receiveValue: { value in
63 | /// try await callAsyncThrowsFunction()
64 | /// })
65 | /// .store(in: &cancellable)
66 | ///
67 | func asyncSinkWithThrows(
68 | receiveCompletionPriority: TaskPriority? = nil,
69 | receiveCompletion: @escaping ((Subscribers.Completion) async throws -> Void),
70 | receiveValuePriority: TaskPriority? = nil,
71 | receiveValue: @escaping ((Self.Output) async throws -> Void)
72 | ) -> Set {
73 | var set = Set()
74 | var tasks = [Task]()
75 |
76 | let cancellable: AnyCancellable = self.sink(
77 | receiveCompletion: { result in
78 | tasks.append(Task(priority: receiveCompletionPriority) {
79 | try Task.checkCancellation()
80 | try await receiveCompletion(result)
81 | })
82 | },
83 | receiveValue: { value in
84 | tasks.append(Task(priority: receiveValuePriority) {
85 | try Task.checkCancellation()
86 | try await receiveValue(value)
87 | })
88 | }
89 | )
90 |
91 | // store cancellable to prevent from canceling stream
92 | cancellable
93 | .store(in: &set)
94 |
95 | // generate task cancellable
96 | cancellable
97 | .cancel {
98 | tasks.forEach { $0.cancel() }
99 | }
100 | .store(in: &set)
101 |
102 | return set
103 | }
104 | }
105 |
106 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
107 | public extension Publisher {
108 |
109 | /// Example
110 | ///
111 | /// Just(99)
112 | /// .asyncMap { number in
113 | /// await callAsyncFunction(number)
114 | /// }
115 | /// .sink { number in
116 | /// // do some handling
117 | /// }
118 | /// .store(in: &cancellable)
119 | ///
120 | func asyncMap(
121 | priority: TaskPriority? = nil,
122 | _ asyncFunction: @escaping (Output) async -> V
123 | ) -> Publishers.FlatMap, Self> {
124 |
125 | flatMap { value in
126 | Future(priority: priority) {
127 | await asyncFunction(value)
128 | }
129 | }
130 | }
131 |
132 | /// Example
133 | ///
134 | /// URL(string: "https....")
135 | /// .publisher
136 | /// .compactMap { $0 }
137 | /// .asyncMapWithThrows {
138 | /// try await URLSession.shared.data(from: $0)
139 | /// }
140 | /// .sink(receiveCompletion: { result in
141 | /// // do some result handling task
142 | /// }, receiveValue: { value in
143 | /// // do some value handling task
144 | /// })
145 | /// .store(in: &cancellable)
146 | ///
147 | func asyncMapWithThrows(
148 | priority: TaskPriority? = nil,
149 | _ asyncFunction: @escaping (Output) async throws -> V
150 | ) -> Publishers.FlatMap, Publishers.SetFailureType> {
151 |
152 | flatMap { value in
153 | Future(priority: priority) {
154 | try await asyncFunction(value)
155 | }
156 | }
157 | }
158 | }
159 |
160 | #endif
161 |
--------------------------------------------------------------------------------
/Sources/CombineAsyncable/Sequence+Store.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Sequence+Store.swift
3 | //
4 | //
5 | // Created by h.tsuruta on 2022/11/15.
6 | //
7 |
8 | #if compiler(>=5.5)
9 | import Combine
10 |
11 | // MARK: - Sequence Extension
12 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
13 | public extension Sequence where Element == AnyCancellable {
14 |
15 | /// Stores this type-erasing cancellable collection in the specified collection.
16 | ///
17 | /// - Parameter collection: The collection in which to store this ``AnyCancellable`` sequence.
18 | func store(
19 | in collection: inout C
20 | ) where C: RangeReplaceableCollection, C.Element == Element {
21 | forEach { $0.store(in: &collection) }
22 | }
23 |
24 | /// Stores this type-erasing cancellable collection in the specified set.
25 | ///
26 | /// - Parameter set: The set in which to store this ``AnyCancellable`` sequence.
27 | func store(in set: inout Set) {
28 | forEach { $0.store(in: &set) }
29 | }
30 |
31 | /// Cancel all AnyCancellable
32 | func cancel() {
33 | forEach { $0.cancel() }
34 | }
35 | }
36 |
37 | #endif
38 |
--------------------------------------------------------------------------------
/Tests/CombineAsyncableTests/PublisherTests.swift:
--------------------------------------------------------------------------------
1 | #if canImport(Combine)
2 |
3 | import XCTest
4 | import Combine
5 | @testable import CombineAsyncable
6 |
7 | // MARK: - XCTestCase
8 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
9 | final class PublisherTests: XCTestCase {
10 |
11 | // MARK: Prorperty
12 |
13 | static var allTests = [
14 | ("testAsyncSink", testAsyncSink),
15 | ("testAsyncSinkWithObject", testAsyncSinkWithObject),
16 | ("testAsyncSinkWithThrows", testAsyncSinkWithThrows)
17 | ]
18 |
19 | // MARK: Test
20 |
21 | func testAsyncSink() async {
22 | let exp = expectation(description: "wait for asyncSink")
23 | let subject = PassthroughSubject()
24 | var cancellables = Set()
25 |
26 | subject.asyncSink(priority: .background) {
27 | // check status when starting
28 | XCTAssertFalse(Task.isCancelled)
29 | // wait `cancel()` method
30 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
31 | // check status after canceling
32 | XCTAssertTrue(Task.isCancelled)
33 | // end task
34 | exp.fulfill()
35 | }.store(in: &cancellables)
36 |
37 | // exec
38 | subject.send(())
39 | // wait 1 seconds to cancel
40 | sleep(1)
41 | cancellables.cancel()
42 |
43 | wait(for: [exp], timeout: 2.0)
44 | }
45 |
46 | func testAsyncSinkWithObject() async {
47 | // test: cancel
48 | do {
49 | let exp = expectation(description: "wait for asyncSink")
50 |
51 | let mock = ObjectMock { exp.fulfill() }
52 | mock.subject.send(())
53 | mock.cancellables.forEach { $0.cancel() }
54 | sleep(1)
55 | XCTAssertTrue(mock.methodCalled)
56 |
57 | wait(for: [exp], timeout: 2)
58 | }
59 | // test: object is nil
60 | do {
61 | let exp = expectation(description: "wait for asyncSink")
62 | exp.isInverted = true
63 |
64 | var mock: ObjectMock? = .init { exp.fulfill() }
65 | mock?.subject.send(())
66 | mock = nil
67 | sleep(1)
68 | XCTAssertNil(mock?.methodCalled)
69 |
70 | wait(for: [exp], timeout: 2)
71 | }
72 | }
73 |
74 | func testAsyncSinkWithThrows() async {
75 | // completion: .finished
76 | do {
77 | let exp = expectation(description: "wait for asyncSinkWithThrows")
78 | exp.expectedFulfillmentCount = 2
79 |
80 | let subject = PassthroughSubject()
81 | var cancellables = Set()
82 |
83 | subject.asyncSinkWithThrows(
84 | receiveCompletionPriority: .background,
85 | receiveCompletion: { result in
86 | switch result {
87 | case .finished:
88 | break
89 | case .failure:
90 | XCTFail("never exec")
91 | }
92 | // check status when starting
93 | XCTAssertFalse(Task.isCancelled)
94 | // wait `cancel()` method
95 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
96 | // check status after canceling
97 | XCTAssertTrue(Task.isCancelled)
98 | // end task
99 | exp.fulfill()
100 | },
101 | receiveValuePriority: .background,
102 | receiveValue: { value in
103 | // check value
104 | XCTAssertEqual(value, 100)
105 | // check status when starting
106 | XCTAssertFalse(Task.isCancelled)
107 | // wait `cancel()` method
108 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
109 | // check status after canceling
110 | XCTAssertTrue(Task.isCancelled)
111 | // end task
112 | exp.fulfill()
113 | }
114 | ).store(in: &cancellables)
115 |
116 | // exec
117 | subject.send(100)
118 | subject.send(completion: .finished)
119 | // wait 1 seconds to cancel
120 | sleep(1)
121 | cancellables.cancel()
122 |
123 | wait(for: [exp], timeout: 2.0)
124 | }
125 | // completion: .failure
126 | do {
127 | let exp = expectation(description: "wait for asyncSinkWithThrows")
128 | exp.expectedFulfillmentCount = 2
129 |
130 | let subject = PassthroughSubject()
131 | var cancellables = Set()
132 |
133 | subject.asyncSinkWithThrows(
134 | receiveCompletionPriority: .background,
135 | receiveCompletion: { result in
136 | switch result {
137 | case .finished:
138 | XCTFail("never exec")
139 | case .failure(let error):
140 | XCTAssertTrue(error is StubError)
141 | }
142 | // check status when starting
143 | XCTAssertFalse(Task.isCancelled)
144 | // wait `cancel()` method
145 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
146 | // check status after canceling
147 | XCTAssertTrue(Task.isCancelled)
148 | // end task
149 | exp.fulfill()
150 | },
151 | receiveValuePriority: .background,
152 | receiveValue: { value in
153 | // check value
154 | XCTAssertEqual(value, 200)
155 | // check status when starting
156 | XCTAssertFalse(Task.isCancelled)
157 | // wait `cancel()` method
158 | try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
159 | // check status after canceling
160 | XCTAssertTrue(Task.isCancelled)
161 | // end task
162 | exp.fulfill()
163 | }
164 | ).store(in: &cancellables)
165 |
166 | // exec
167 | subject.send(200)
168 | subject.send(completion: .failure(StubError()))
169 | // wait 1 seconds to cancel
170 | sleep(1)
171 | cancellables.cancel()
172 |
173 | wait(for: [exp], timeout: 2.0)
174 | }
175 | }
176 | }
177 |
178 | // MARK: - Mock
179 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
180 | private final class ObjectMock {
181 |
182 | // MARK: Test flag
183 |
184 | private(set) var methodCalled = false
185 | func callAsync(completion: @escaping () -> Void) async {
186 | methodCalled = true
187 | completion()
188 | }
189 |
190 | // MARK: Implementation
191 |
192 | let subject = PassthroughSubject()
193 | var cancellables = Set()
194 |
195 | init(completion: @escaping () -> Void) {
196 | subject.asyncSink(priority: .background) { [weak self] in
197 | guard let self = self else { return }
198 | await self.callAsync(completion: completion)
199 | }.store(in: &cancellables)
200 | }
201 | }
202 |
203 | #endif
204 |
--------------------------------------------------------------------------------
/Tests/CombineAsyncableTests/StubError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StubError.swift
3 | //
4 | //
5 | // Created by h.tsuruta on 2022/11/30.
6 | //
7 |
8 | import Foundation
9 |
10 | // MARK: - Stub Error
11 | final class StubError: Error {}
12 |
--------------------------------------------------------------------------------
/Tests/CombineAsyncableTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(PublisherTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | import CombineAsyncableTests
3 |
4 | var tests = [XCTestCaseEntry]()
5 | tests += CombineAsyncableTests.allTests()
6 | XCTMain(tests)
7 |
--------------------------------------------------------------------------------