Repository: groue/CombineExpectations Branch: master Commit: 04d4e4b21c9e Files: 27 Total size: 181.0 KB Directory structure: gitextract_q_moxppx/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── CombineExpectations.podspec ├── LICENSE ├── Package.swift ├── README.md ├── Sources/ │ └── CombineExpectations/ │ ├── PublisherExpectation.swift │ ├── PublisherExpectations/ │ │ ├── AvailableElements.swift │ │ ├── Finished.swift │ │ ├── Inverted.swift │ │ ├── Map.swift │ │ ├── Next.swift │ │ ├── NextOne.swift │ │ ├── Prefix.swift │ │ └── Recording.swift │ ├── Recorder.swift │ └── RecordingError.swift └── Tests/ ├── CombineExpectationsTests/ │ ├── DocumentationTests.swift │ ├── FailureTestCase.swift │ ├── LateSubscriptionTest.swift │ ├── RecorderTests.swift │ ├── Support.swift │ ├── WackySubscriberTests.swift │ └── XCTestManifests.swift └── LinuxMain.swift ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [groue] ================================================ FILE: .github/workflows/ci.yml ================================================ name: Continuous Integration on: push: branches: [ master ] pull_request: branches: [ master ] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-test: name: Build & Test (Swift ${{ matrix.swift }}, ${{ matrix.platform }}) runs-on: macos-${{ matrix.macos || '11' }} strategy: fail-fast: false matrix: swift: ['5.1', '5.2', '5.3', '5.4', '5.5'] platform: [macOS, iOS, tvOS, watchOS] exclude: # watchOS requires Swift 5.4 or later. - swift: '5.1' platform: watchOS - swift: '5.2' platform: watchOS - swift: '5.3' platform: watchOS include: # The macOS 11 runner no longer includes Swift 5.1. - swift: '5.1' macos: '10.15' steps: - name: Clone uses: actions/checkout@v2 - name: Build & Test uses: mxcl/xcodebuild@v1 with: swift: ~${{ matrix.swift }} platform: ${{ matrix.platform }} ================================================ FILE: .gitignore ================================================ .DS_Store /.build /Packages /*.xcodeproj xcuserdata/ /.swiftpm ================================================ FILE: CHANGELOG.md ================================================ Release Notes ============= All notable changes to this project will be documented in this file. #### 0.x Releases - [0.10.0](#0100) - [0.9.0](#090) - [0.8.0](#080) - [0.7.0](#070) - [0.6.0](#060) - [0.5.0](#050) - [0.4.0](#040) - [0.3.0](#030) - [0.2.0](#020) - [0.1.0](#010) ## 0.10.0 Released August 11, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.9.0...v0.10.0) - **New**: [#17](https://github.com/groue/CombineExpectations/pull/17) by [@chris-araman](https://github.com/chris-araman): Improvements for Swift 5.1, Swift 5.3, Swift 5.4, watchOS 7.4, Xcode 12.5, Xcode 13 beta ## 0.9.0 Released June 7, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.8.0...v0.9.0) - **New**: [#16](https://github.com/groue/CombineExpectations/pull/16) by [@chris-araman](https://github.com/chris-araman): Support watchOS 6 or later when building with Swift 5.4 or later ## 0.8.0 Released May 29, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.7.0...v0.8.0) - **Fixed**: [#15](https://github.com/groue/CombineExpectations/pull/15) by [@chrisballinger](https://github.com/chrisballinger): Fix XCTFail not found issue with Xcode 12.5 ## 0.7.0 Released January 9, 2021 • [diff](https://github.com/groue/CombineExpectations/compare/v0.6.0...v0.7.0) - **Fixed**: [#13](https://github.com/groue/CombineExpectations/pull/13) by [@chrisballinger](https://github.com/chrisballinger): Remove module_name override in CocoaPods spec ## 0.6.0 Released December 23, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.5.0...v0.6.0) - **New**: [#11](https://github.com/groue/CombineExpectations/pull/11): `availableElements` expectation (fixes [#8](https://github.com/groue/CombineExpectations/issues/8)). ## 0.5.0 Released June 25, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.4.0...v0.5.0) - **Fixed**: `next().get()` no longer returns an optional. - **New**: Support for Xcode 12 ## 0.4.0 Released January 4, 2020 • [diff](https://github.com/groue/CombineExpectations/compare/v0.3.0...v0.4.0) - [#6](https://github.com/groue/CombineExpectations/pull/6): Support for synchronous tests **Documentation diff**: The [Usage] section shows how to use the new `get()` method in order to perform synchronous tests that do not have to wait. ## 0.3.0 Released November 27, 2019 • [diff](https://github.com/groue/CombineExpectations/compare/v0.2.0...v0.3.0) - [#2](https://github.com/groue/CombineExpectations/pull/2): RecordingError renaming - [#3](https://github.com/groue/CombineExpectations/pull/3): Next Expectation - [#4](https://github.com/groue/CombineExpectations/pull/4): Drop the "first" expectation ## 0.2.0 Released November 24, 2019 • [diff](https://github.com/groue/CombineExpectations/compare/v0.1.0...v0.2.0) **Increased robustness** ## 0.1.0 Released November 23, 2019 **Initial release** [Usage]: README.md#usage ================================================ FILE: CombineExpectations.podspec ================================================ Pod::Spec.new do |s| s.name = 'CombineExpectations' s.version = '0.10.0' s.license = { :type => 'MIT', :file => 'LICENSE' } s.summary = 'A set of extensions for SQLite, GRDB.swift, and Combine' s.homepage = 'https://github.com/groue/CombineExpectations' s.author = { 'Gwendal Roué' => 'gr@pierlis.com' } s.source = { :git => 'https://github.com/groue/CombineExpectations.git', :tag => "v#{s.version}" } s.swift_versions = ['5.1', '5.2', '5.3', '5.4'] s.ios.deployment_target = '13.0' s.osx.deployment_target = '10.15' s.tvos.deployment_target = '13.0' s.watchos.deployment_target = '7.4' s.frameworks = ['Combine', 'XCTest'] s.source_files = 'Sources/CombineExpectations/**/*.swift' s.pod_target_xcconfig = { "ENABLE_TESTING_SEARCH_PATHS" => "YES" # Required for Xcode 12.5 } end ================================================ FILE: LICENSE ================================================ Copyright (C) 2019 Gwendal Roué Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Package.swift ================================================ // swift-tools-version:5.1 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "CombineExpectations", platforms: [ .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), ], products: [ // Products define the executables and libraries produced by a package, and make them visible to other packages. .library( name: "CombineExpectations", targets: ["CombineExpectations"]), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. // Targets can depend on other targets in this package, and on products in packages which this package depends on. .target( name: "CombineExpectations", dependencies: [], linkerSettings: [.linkedFramework("XCTest")]), .testTarget( name: "CombineExpectationsTests", dependencies: ["CombineExpectations"]), ] ) #if swift(>=5.4) // XCTest was introduced for watchOS with Swift 5.4, Xcode 12.5, and watchOS 7.4. package.platforms! += [ .watchOS("7.4") ] #endif ================================================ FILE: README.md ================================================ # Combine Expectations ### Utilities for tests that wait for Combine publishers. --- **Latest release**: [version 0.10.0](https://github.com/groue/CombineExpectations/tree/v0.10.0) (August 11, 2021) • [Release Notes] **Requirements**: iOS 13+, macOS 10.15+, and tvOS 13+ require Swift 5.1+ or Xcode 11+. watchOS 7.4+ requires Swift 5.4+ or Xcode 12.5+. **Contact**: Report bugs and ask questions in [Github issues](https://github.com/groue/CombineExpectations/issues). --- Testing Combine publishers with [XCTestExpectation](*https://developer.apple.com/documentation/xctest/xctestexpectation*) often requires setting up a lot of boilerplate code. CombineExpectations aims at streamlining those tests. It defines an XCTestCase method which waits for *publisher expectations*. - [Usage] - [Installation] - [Publisher Expectations]: [availableElements], [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single] --- ## Usage Waiting for [Publisher Expectations] allows your tests to look like this: ```swift import XCTest import CombineExpectations class PublisherTests: XCTestCase { func testElements() throws { // 1. Create a publisher let publisher = ... // 2. Start recording the publisher let recorder = publisher.record() // 3. Wait for a publisher expectation let elements = try wait(for: recorder.elements, timeout: ..., description: "Elements") // 4. Test the result of the expectation XCTAssertEqual(elements, ["Hello", "World!"]) } } ``` **When you wait for a publisher expectation:** - The test fails if the expectation is not fulfilled within the specified timeout. - An error is thrown if the expected value can not be returned. For example, waiting for `recorder.elements` throws the publisher error if the publisher completes with a failure. - The `wait` method returns immediately if the expectation has already reached the waited state. You can wait multiple times for a publisher: ```swift class PublisherTests: XCTestCase { func testPublisher() throws { let publisher = ... let recorder = publisher.record() // Wait for first element _ = try wait(for: recorder.next(), timeout: ...) // Wait for second element _ = try wait(for: recorder.next(), timeout: ...) // Wait for successful completion try wait(for: recorder.finished, timeout: ...) } } ``` **Not all tests have to wait**, because some publishers expectations are fulfilled right away. In this case, prefer the synchronous `get()` method over `wait(for:timeout:)`, as below: ```swift class PublisherTests: XCTestCase { func testSynchronousPublisher() throws { // 1. Create a publisher let publisher = ... // 2. Start recording the publisher let recorder = publisher.record() // 3. Grab the expected result let elements = try recorder.elements.get() // 4. Test the result of the expectation XCTAssertEqual(elements, ["Hello", "World!"]) } } ``` Just like `wait(for:timeout:)`, the `get()` method can be called multiple times: ```swift class PublisherTests: XCTestCase { // SUCCESS: no error func testPassthroughSubjectSynchronouslyPublishesElements() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") try XCTAssertEqual(recorder.next().get(), "foo") publisher.send("bar") try XCTAssertEqual(recorder.next().get(), "bar") } } ``` ## Installation Add a dependency for CombineExpectations to your [Swift Package](https://swift.org/package-manager/) test targets: ```diff import PackageDescription let package = Package( dependencies: [ + .package(url: "https://github.com/groue/CombineExpectations.git", ...) ], targets: [ .testTarget( dependencies: [ + "CombineExpectations" ]) ] ) ``` ## Publisher Expectations There are various publisher expectations. Each one waits for a specific publisher aspect: - [availableElements]: all published elements until timeout expiration - [completion]: the publisher completion - [elements]: all published elements until successful completion - [finished]: the publisher successful completion - [last]: the last published element - [next()]: the next published element - [next(count)]: the next N published elements - [prefix(maxLength)]: the first N published elements - [recording]: the full recording of publisher events - [single]: the one and only published element --- ### availableElements :clock230: `recorder.availableElements` waits for the expectation to expire, or the recorded publisher to complete. :x: When waiting for this expectation, the publisher error is thrown if the publisher fails before the expectation has expired. :white_check_mark: Otherwise, an array of all elements published before the expectation has expired is returned. :arrow_right: Related expectations: [elements], [prefix(maxLength)]. Unlike other expectations, `availableElements` does not make a test fail on timeout expiration. It just returns the elements published so far. Example: ```swift // SUCCESS: no timeout, no error func testTimerPublishesIncreasingDates() throws { let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() let recorder = publisher.record() let dates = try wait(for: recorder.availableElements, timeout: ...) XCTAssertEqual(dates.sorted(), dates) } ``` ### completion :clock230: `recorder.completion` waits for the recorded publisher to complete. :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time. :white_check_mark: Otherwise, a [`Subscribers.Completion`](https://developer.apple.com/documentation/combine/subscribers/completion) is returned. :arrow_right: Related expectations: [finished], [recording]. Example: ```swift // SUCCESS: no timeout, no error func testArrayPublisherCompletesWithSuccess() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: ...) if case let .failure(error) = completion { XCTFail("Unexpected error \(error)") } } // SUCCESS: no error func testArrayPublisherSynchronouslyCompletesWithSuccess() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let completion = try recorder.completion.get() if case let .failure(error) = completion { XCTFail("Unexpected error \(error)") } } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testCompletionTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: ...) } ```
--- ### elements :clock230: `recorder.elements` waits for the recorded publisher to complete. :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails. :white_check_mark: Otherwise, an array of published elements is returned. :arrow_right: Related expectations: [availableElements], [last], [prefix(maxLength)], [recording], [single]. Example: ```swift // SUCCESS: no timeout, no error func testArrayPublisherPublishesArrayElements() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: ...) XCTAssertEqual(elements, ["foo", "bar", "baz"]) } // SUCCESS: no error func testArrayPublisherSynchronouslyPublishesArrayElements() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try recorder.elements.get() XCTAssertEqual(elements, ["foo", "bar", "baz"]) } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testElementsTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: ...) } // FAIL: Caught error MyError func testElementsError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.elements, timeout: ...) } ```
--- ### finished :clock230: `recorder.finished` waits for the recorded publisher to complete. :x: When waiting for this expectation, the publisher error is thrown if the publisher fails. :arrow_right: Related expectations: [completion], [recording]. Example: ```swift // SUCCESS: no timeout, no error func testArrayPublisherFinishesWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() try wait(for: recorder.finished, timeout: ...) } // SUCCESS: no error func testArrayPublisherSynchronouslyFinishesWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() try recorder.finished.get() } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed func testFinishedTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.finished, timeout: ...) } // FAIL: Caught error MyError func testFinishedError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished, timeout: ...) } ```
`recorder.finished` can be inverted: ```swift // SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotFinish() throws { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: ...) } ```
Examples of failing tests ```swift // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedFinishedError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished.inverted, timeout: ...) } ```
--- ### last :clock230: `recorder.last` waits for the recorded publisher to complete. :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails. :white_check_mark: Otherwise, the last published element is returned, or nil if the publisher completes before it publishes any element. :arrow_right: Related expectations: [elements], [single]. Example: ```swift // SUCCESS: no timeout, no error func testArrayPublisherPublishesLastElementLast() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() if let element = try wait(for: recorder.last, timeout: ...) { XCTAssertEqual(element, "baz") } else { XCTFail("Expected one element") } } // SUCCESS: no error func testArrayPublisherSynchronouslyPublishesLastElementLast() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() if let element = try recorder.last.get() { XCTAssertEqual(element, "baz") } else { XCTFail("Expected one element") } } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testLastTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: ...) } // FAIL: Caught error MyError func testLastError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let element = try wait(for: recorder.last, timeout: ...) } ```
--- ### next() :clock230: `recorder.next()` waits for the recorded publisher to emit one element, or to complete. :x: When waiting for this expectation, a `RecordingError.notEnoughElements` is thrown if the publisher does not publish one element after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next element. :white_check_mark: Otherwise, the next published element is returned. :arrow_right: Related expectations: [next(count)], [single]. Example: ```swift // SUCCESS: no timeout, no error func testArrayOfTwoElementsPublishesElementsInOrder() throws { let publisher = ["foo", "bar"].publisher let recorder = publisher.record() var element = try wait(for: recorder.next(), timeout: ...) XCTAssertEqual(element, "foo") element = try wait(for: recorder.next(), timeout: ...) XCTAssertEqual(element, "bar") } // SUCCESS: no error func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { let publisher = ["foo", "bar"].publisher let recorder = publisher.record() var element = try recorder.next().get() XCTAssertEqual(element, "foo") element = try recorder.next().get() XCTAssertEqual(element, "bar") } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: ...) } // FAIL: Caught error MyError func testNextError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let element = try wait(for: recorder.next(), timeout: ...) } // FAIL: Caught error RecordingError.notEnoughElements func testNextNotEnoughElementsError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .finished) let element = try wait(for: recorder.next(), timeout: ...) } ```
`recorder.next()` can be inverted: ```swift // SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotPublishAnyElement() throws { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: ...) } ```
Examples of failing tests ```swift // FAIL: Fulfilled inverted expectation func testInvertedNextTooEarly() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") try wait(for: recorder.next().inverted, timeout: ...) } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedNextError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.next().inverted, timeout: ...) } ```
--- ### next(count) :clock230: `recorder.next(count)` waits for the recorded publisher to emit `count` elements, or to complete. :x: When waiting for this expectation, a `RecordingError.notEnoughElements` is thrown if the publisher does not publish `count` elements after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next `count` elements. :white_check_mark: Otherwise, an array of exactly `count` elements is returned. :arrow_right: Related expectations: [next()], [prefix(maxLength)]. Example: ```swift // SUCCESS: no timeout, no error func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() var elements = try wait(for: recorder.next(2), timeout: ...) XCTAssertEqual(elements, ["foo", "bar"]) elements = try wait(for: recorder.next(1), timeout: ...) XCTAssertEqual(elements, ["baz"]) } // SUCCESS: no error func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() var elements = try recorder.next(2).get() XCTAssertEqual(elements, ["foo", "bar"]) elements = try recorder.next(1).get() XCTAssertEqual(elements, ["baz"]) } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextCountTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") let elements = try wait(for: recorder.next(2), timeout: ...) } // FAIL: Caught error MyError func testNextCountError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.next(2), timeout: ...) } // FAIL: Caught error RecordingError.notEnoughElements func testNextCountNotEnoughElementsError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .finished) let elements = try wait(for: recorder.next(2), timeout: ...) } ```
--- ### prefix(maxLength) :clock230: `recorder.prefix(maxLength)` waits for the recorded publisher to emit `maxLength` elements, or to complete. :x: When waiting for this expectation, the publisher error is thrown if the publisher fails before `maxLength` elements are published. :white_check_mark: Otherwise, an array of received elements is returned, containing at most `maxLength` elements, or less if the publisher completes early. :arrow_right: Related expectations: [availableElements], [elements], [next(count)]. Example: ```swift // SUCCESS: no timeout, no error func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: ...) XCTAssertEqual(elements, ["foo", "bar"]) } // SUCCESS: no error func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try recorder.prefix(2).get() XCTAssertEqual(elements, ["foo", "bar"]) } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed func testPrefixTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") let elements = try wait(for: recorder.prefix(2), timeout: ...) } // FAIL: Caught error MyError func testPrefixError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.prefix(2), timeout: ...) } ```
`recorder.prefix(maxLength)` can be inverted: ```swift // SUCCESS: no timeout, no error func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) XCTAssertEqual(elements, ["foo", "bar"]) } ```
Examples of failing tests ```swift // FAIL: Fulfilled inverted expectation func testInvertedPrefixTooEarly() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") publisher.send("baz") let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedPrefixError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) } ```
--- ### recording :clock230: `recorder.recording` waits for the recorded publisher to complete. :x: When waiting for this expectation, a `RecordingError.notCompleted` is thrown if the publisher does not complete on time. :white_check_mark: Otherwise, a [`Record.Recording`](https://developer.apple.com/documentation/combine/record/recording) is returned. :arrow_right: Related expectations: [completion], [elements], [finished]. Example: ```swift // SUCCESS: no timeout, no error func testArrayPublisherRecording() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: ...) XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } // SUCCESS: no error func testArrayPublisherSynchronousRecording() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let recording = try recorder.recording.get() XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testRecordingTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: ...) } ```
--- ### single :clock230: `recorder.single` waits for the recorded publisher to complete. :x: When waiting for this expectation, a `RecordingError` is thrown if the publisher does not complete on time, or does not publish exactly one element before it completes. The publisher error is thrown if the publisher fails. :white_check_mark: Otherwise, the single published element is returned. :arrow_right: Related expectations: [elements], [last], [next()]. Example: ```swift // SUCCESS: no timeout, no error func testJustPublishesExactlyOneElement() throws { let publisher = Just("foo") let recorder = publisher.record() let element = try wait(for: recorder.single, timeout: ...) XCTAssertEqual(element, "foo") } // SUCCESS: no error func testJustSynchronouslyPublishesExactlyOneElement() throws { let publisher = Just("foo") let recorder = publisher.record() let element = try recorder.single.get() XCTAssertEqual(element, "foo") } ```
Examples of failing tests ```swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testSingleTimeout() throws { let publisher = PassthroughSubject() let recorder = publisher.record() let element = try wait(for: recorder.single, timeout: ...) } // FAIL: Caught error MyError func testSingleError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let element = try wait(for: recorder.single, timeout: ...) } // FAIL: Caught error RecordingError.tooManyElements func testSingleTooManyElementsError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") publisher.send(completion: .finished) let element = try wait(for: recorder.single, timeout: ...) } // FAIL: Caught error RecordingError.notEnoughElements func testSingleNotEnoughElementsError() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .finished) let element = try wait(for: recorder.single, timeout: ...) } ```
[Release Notes]: CHANGELOG.md [Usage]: #usage [Installation]: #installation [Publisher Expectations]: #publisher-expectations [finished]: #finished [prefix(maxLength)]: #prefixmaxlength [next()]: #next [next(count)]: #nextcount [recording]: #recording [completion]: #completion [elements]: #elements [last]: #last [single]: #single [availableElements]: #availableElements ================================================ FILE: Sources/CombineExpectations/PublisherExpectation.swift ================================================ import XCTest /// A name space for publisher expectations public enum PublisherExpectations { } /// The base protocol for PublisherExpectation. It is an implementation detail /// that you are not supposed to use, as shown by the underscore prefix. /// /// :nodoc: public protocol _PublisherExpectationBase { /// Sets up an XCTestExpectation. This method is an implementation detail /// that you are not supposed to use, as shown by the underscore prefix. func _setup(_ expectation: XCTestExpectation) /// Returns an object that waits for the expectation. If nil, expectation /// is waited by the XCTestCase. func _makeWaiter() -> XCTWaiter? } extension _PublisherExpectationBase { /// :nodoc: public func _makeWaiter() -> XCTWaiter? { nil } } /// The protocol for publisher expectations. /// /// You can build publisher expectations from Recorder returned by the /// `Publisher.record()` method. /// /// For example: /// /// // The expectation for all published elements until completion /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let expectation = recorder.elements /// /// When a test grants some time for the expectation to fulfill, use the /// XCTest `wait(for:timeout:description)` method: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherPublishesArrayElements() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let expectation = recorder.elements /// let elements = try wait(for: expectation, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) /// } /// /// On the other hand, when the expectation is supposed to be immediately /// fulfilled, use the PublisherExpectation `get()` method in order to grab the /// expected value: /// /// // SUCCESS: no error /// func testArrayPublisherSynchronouslyPublishesArrayElements() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try recorder.elements.get() /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) /// } public protocol PublisherExpectation: _PublisherExpectationBase { /// The type of the expected value. associatedtype Output /// Returns the expected value, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayPublisherSynchronouslyPublishesArrayElements() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try recorder.elements.get() /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) /// } func get() throws -> Output } extension XCTestCase { /// Waits for the publisher expectation to fulfill, and returns the /// expected value. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherPublishesArrayElements() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) /// } /// /// - parameter publisherExpectation: The publisher expectation. /// - parameter timeout: The number of seconds within which the expectation /// must be fulfilled. /// - parameter description: A string to display in the test log for the /// expectation, to help diagnose failures. /// - throws: An error if the expectation fails. public func wait( for publisherExpectation: R, timeout: TimeInterval, description: String = "") throws -> R.Output { let expectation = self.expectation(description: description) publisherExpectation._setup(expectation) if let waiter = publisherExpectation._makeWaiter() { waiter.wait(for: [expectation], timeout: timeout) } else { wait(for: [expectation], timeout: timeout) } return try publisherExpectation.get() } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/AvailableElements.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation which waits for the timeout to expire, or /// the recorded publisher to complete. /// /// When waiting for this expectation, the publisher error is thrown if /// the publisher fails before the expectation has expired. /// /// Otherwise, an array of all elements published before the expectation /// has expired is returned. /// /// Unlike other expectations, `AvailableElements` does not make a test fail /// on timeout expiration. It just returns the elements published so far. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testTimerPublishesIncreasingDates() throws { /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() /// let recorder = publisher.record() /// let dates = try wait(for: recorder.availableElements, timeout: ...) /// XCTAssertEqual(dates.sorted(), dates) /// } public struct AvailableElements: PublisherExpectation { let recorder: Recorder public func _makeWaiter() -> XCTWaiter? { Waiter() } public func _setup(_ expectation: XCTestExpectation) { recorder.fulfillOnCompletion(expectation) } /// Returns all elements published so far, or throws an error if the /// publisher has failed. public func get() throws -> [Input] { try recorder.value { (elements, completion, remainingElements, consume) in if case let .failure(error) = completion { throw error } consume(remainingElements.count) return elements } } /// A waiter that waits but never fails private class Waiter: XCTWaiter, XCTWaiterDelegate { init() { super.init(delegate: nil) delegate = self } func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { } func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { } func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { } func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { } } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Finished.swift ================================================ import XCTest // The Finished expectation waits for the publisher to complete, and throws an // error if and only if the publisher fails with an error. // // It is not derived from the Recording expectation, because Finished does not // throw RecordingError.notCompleted if the publisher does not complete on time. // It only triggers a timeout test failure. // // This allows to write tests for publishers that should not complete: // // // SUCCESS: no timeout, no error // func testPassthroughSubjectDoesNotFinish() throws { // let publisher = PassthroughSubject() // let recorder = publisher.record() // try wait(for: recorder.finished.inverted, timeout: 1) // } extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, the publisher error is thrown if the /// publisher fails. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherFinishesWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// try wait(for: recorder.finished, timeout: 1) /// } /// /// This publisher expectation can be inverted: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotFinish() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.finished.inverted, timeout: 1) /// } public struct Finished: PublisherExpectation { let recorder: Recorder public func _setup(_ expectation: XCTestExpectation) { recorder.fulfillOnCompletion(expectation) } /// Returns the expected output, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayPublisherSynchronouslyFinishesWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// try recorder.finished.get() /// } public func get() throws { try recorder.value { (_, completion, remainingElements, consume) in guard let completion = completion else { consume(remainingElements.count) return } if case let .failure(error) = completion { throw error } } } /// Returns an inverted publisher expectation which waits for a /// publisher to complete successfully. /// /// When waiting for this expectation, an error is thrown if the /// publisher fails with an error. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotFinish() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.finished.inverted, timeout: 1) /// } public var inverted: Inverted { return Inverted(base: self) } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Inverted.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation that fails if the base expectation is fulfilled. /// /// When waiting for this expectation, you receive the same result and /// eventual error as the base expectation. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotFinish() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.finished.inverted, timeout: 1) /// } public struct Inverted: PublisherExpectation { let base: Base public func _setup(_ expectation: XCTestExpectation) { base._setup(expectation) expectation.isInverted.toggle() } public func get() throws -> Base.Output { try base.get() } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Map.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation that transforms the value of a base expectation. /// /// This expectation has no public initializer. public struct Map: PublisherExpectation { let base: Base let transform: (Base.Output) throws -> Output public func _setup(_ expectation: XCTestExpectation) { base._setup(expectation) } public func get() throws -> Output { try transform(base.get()) } } } extension PublisherExpectation { /// Returns a publisher expectation that transforms the value of the /// base expectation. func map(_ transform: @escaping (Output) throws -> T) -> PublisherExpectations.Map { PublisherExpectations.Map(base: self, transform: transform) } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Next.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `count` elements, or to complete. /// /// When waiting for this expectation, a `RecordingError.notEnoughElements` /// is thrown if the publisher does not publish `count` elements after last /// waited expectation. The publisher error is thrown if the publisher fails /// before publishing the next `count` elements. /// /// Otherwise, an array of exactly `count` elements is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// /// var elements = try wait(for: recorder.next(2), timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// /// elements = try wait(for: recorder.next(1), timeout: 1) /// XCTAssertEqual(elements, ["baz"]) /// } public struct Next: PublisherExpectation { let recorder: Recorder let count: Int init(recorder: Recorder, count: Int) { precondition(count >= 0, "Can't take a prefix of negative length") self.recorder = recorder self.count = count } public func _setup(_ expectation: XCTestExpectation) { if count == 0 { // Such an expectation is immediately fulfilled, by essence. expectation.expectedFulfillmentCount = 1 expectation.fulfill() } else { expectation.expectedFulfillmentCount = count recorder.fulfillOnInput(expectation, includingConsumed: false) } } /// Returns the expected output, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// /// var elements = try recorder.next(2).get() /// XCTAssertEqual(elements, ["foo", "bar"]) /// /// elements = try recorder.next(1).get() /// XCTAssertEqual(elements, ["baz"]) /// } public func get() throws -> [Input] { try recorder.value { (_, completion, remainingElements, consume) in if remainingElements.count >= count { consume(count) return Array(remainingElements.prefix(count)) } if case let .failure(error) = completion { throw error } else { throw RecordingError.notEnoughElements } } } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/NextOne.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// one element, or to complete. /// /// When waiting for this expectation, a `RecordingError.notEnoughElements` /// is thrown if the publisher does not publish one element after last /// waited expectation. The publisher error is thrown if the publisher fails /// before publishing the next element. /// /// Otherwise, the next published element is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfTwoElementsPublishesElementsInOrder() throws { /// let publisher = ["foo", "bar"].publisher /// let recorder = publisher.record() /// /// var element = try wait(for: recorder.next(), timeout: 1) /// XCTAssertEqual(element, "foo") /// /// element = try wait(for: recorder.next(), timeout: 1) /// XCTAssertEqual(element, "bar") /// } public struct NextOne: PublisherExpectation { let recorder: Recorder public func _setup(_ expectation: XCTestExpectation) { recorder.fulfillOnInput(expectation, includingConsumed: false) } /// Returns the expected output, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { /// let publisher = ["foo", "bar"].publisher /// let recorder = publisher.record() /// /// var element = try recorder.next().get() /// XCTAssertEqual(element, "foo") /// /// element = try recorder.next().get() /// XCTAssertEqual(element, "bar") /// } public func get() throws -> Input { try recorder.value { (_, completion, remainingElements, consume) in if let next = remainingElements.first { consume(1) return next } if case let .failure(error) = completion { throw error } else { throw RecordingError.notEnoughElements } } } /// Returns an inverted publisher expectation which waits for the /// recorded publisher to emit one element, or to complete. /// /// When waiting for this expectation, a RecordingError is thrown if the /// publisher does not publish one element after last waited /// expectation. The publisher error is thrown if the publisher fails /// before publishing one element. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotPublishAnyElement() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.next().inverted, timeout: 1) /// } public var inverted: NextOneInverted { return NextOneInverted(recorder: recorder) } } /// An inverted publisher expectation which waits for the recorded publisher /// to emit one element, or to complete. /// /// When waiting for this expectation, a RecordingError is thrown if the /// publisher does not publish one element after last waited expectation. /// The publisher error is thrown if the publisher fails before /// publishing one element. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotPublishAnyElement() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.next().inverted, timeout: 1) /// } public struct NextOneInverted: PublisherExpectation { let recorder: Recorder public func _setup(_ expectation: XCTestExpectation) { expectation.isInverted = true recorder.fulfillOnInput(expectation, includingConsumed: false) } public func get() throws { try recorder.value { (_, completion, remainingElements, consume) in if remainingElements.isEmpty == false { return } if case let .failure(error) = completion { throw error } } } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Prefix.swift ================================================ import XCTest extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher to emit /// `maxLength` elements, or to complete. /// /// When waiting for this expectation, the publisher error is thrown if the /// publisher fails before `maxLength` elements are published. /// /// Otherwise, an array of received elements is returned, containing at /// most `maxLength` elements, or less if the publisher completes early. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try wait(for: recorder.prefix(2), timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// } /// /// This publisher expectation can be inverted: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// publisher.send("foo") /// publisher.send("bar") /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// } public struct Prefix: PublisherExpectation { let recorder: Recorder let maxLength: Int init(recorder: Recorder, maxLength: Int) { precondition(maxLength >= 0, "Can't take a prefix of negative length") self.recorder = recorder self.maxLength = maxLength } public func _setup(_ expectation: XCTestExpectation) { if maxLength == 0 { // Such an expectation is immediately fulfilled, by essence. expectation.expectedFulfillmentCount = 1 expectation.fulfill() } else { expectation.expectedFulfillmentCount = maxLength recorder.fulfillOnInput(expectation, includingConsumed: true) } } /// Returns the expected output, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try recorder.prefix(2).get() /// XCTAssertEqual(elements, ["foo", "bar"]) /// } public func get() throws -> [Input] { try recorder.value { (elements, completion, remainingElements, consume) in if elements.count >= maxLength { let extraCount = max(maxLength + remainingElements.count - elements.count, 0) consume(extraCount) return Array(elements.prefix(maxLength)) } if case let .failure(error) = completion { throw error } consume(remainingElements.count) return elements } } /// Returns an inverted publisher expectation which waits for a /// publisher to emit `maxLength` elements, or to complete. /// /// When waiting for this expectation, the publisher error is thrown /// if the publisher fails before `maxLength` elements are published. /// /// Otherwise, an array of received elements is returned, containing at /// most `maxLength` elements, or less if the publisher completes early. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// publisher.send("foo") /// publisher.send("bar") /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// } public var inverted: Inverted { return Inverted(base: self) } } } ================================================ FILE: Sources/CombineExpectations/PublisherExpectations/Recording.swift ================================================ import Combine import XCTest extension PublisherExpectations { /// A publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError.notCompleted is /// thrown if the publisher does not complete on time. /// /// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording) /// is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherRecording() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let recording = try wait(for: recorder.recording, timeout: 1) /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) /// if case let .failure(error) = recording.completion { /// XCTFail("Unexpected error \(error)") /// } /// } public struct Recording: PublisherExpectation { let recorder: Recorder public func _setup(_ expectation: XCTestExpectation) { recorder.fulfillOnCompletion(expectation) } /// Returns the expected output, or throws an error if the /// expectation fails. /// /// For example: /// /// // SUCCESS: no error /// func testArrayPublisherSynchronousRecording() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let recording = try recorder.recording.get() /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) /// if case let .failure(error) = recording.completion { /// XCTFail("Unexpected error \(error)") /// } /// } public func get() throws -> Record.Recording { try recorder.value { (elements, completion, remainingElements, consume) in if let completion = completion { consume(remainingElements.count) return Record.Recording(output: elements, completion: completion) } else { throw RecordingError.notCompleted } } } } } ================================================ FILE: Sources/CombineExpectations/Recorder.swift ================================================ import Combine import XCTest /// A Combine subscriber which records all events published by a publisher. /// /// You create a Recorder with the `Publisher.record()` method: /// /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// /// You can build publisher expectations from the Recorder. For example: /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) public class Recorder: Subscriber { public typealias Input = Input public typealias Failure = Failure private enum RecorderExpectation { case onInput(XCTestExpectation, remainingCount: Int) case onCompletion(XCTestExpectation) var expectation: XCTestExpectation { switch self { case let .onCompletion(expectation): return expectation case let .onInput(expectation, remainingCount: _): return expectation } } } /// The recorder state private enum State { /// Publisher is not subscribed yet. The recorder may have an /// expectation to fulfill. case waitingForSubscription(RecorderExpectation?) /// Publisher is subscribed. The recorder may have an expectation to /// fulfill. It keeps track of all published elements. case subscribed(Subscription, RecorderExpectation?, [Input]) /// Publisher is completed. The recorder keeps track of all published /// elements and completion. case completed([Input], Subscribers.Completion) var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion?) { switch self { case .waitingForSubscription: return (elements: [], completion: nil) case let .subscribed(_, _, elements): return (elements: elements, completion: nil) case let .completed(elements, completion): return (elements: elements, completion: completion) } } var recorderExpectation: RecorderExpectation? { switch self { case let .waitingForSubscription(exp), let .subscribed(_, exp, _): return exp case .completed: return nil } } } private let lock = NSLock() private var state = State.waitingForSubscription(nil) private var consumedCount = 0 /// The elements and completion recorded so far. var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion?) { synchronized { state.elementsAndCompletion } } /// Use Publisher.record() fileprivate init() { } deinit { if case let .subscribed(subscription, _, _) = state { subscription.cancel() } } private func synchronized(_ execute: () throws -> T) rethrows -> T { lock.lock() defer { lock.unlock() } return try execute() } // MARK: - PublisherExpectation API /// Registers the expectation so that it gets fulfilled when publisher /// publishes elements or completes. /// /// - parameter expectation: An XCTestExpectation. /// - parameter includingConsumed: This flag controls how elements that were /// already published at the time this method is called fulfill the /// expectation. If true, all published elements fulfill the expectation. /// If false, only published elements that are not consumed yet fulfill /// the expectation. For example, the Prefix expectation uses true, but /// the NextOne expectation uses false. func fulfillOnInput(_ expectation: XCTestExpectation, includingConsumed: Bool) { synchronized { preconditionCanFulfillExpectation() let expectedFulfillmentCount = expectation.expectedFulfillmentCount switch state { case .waitingForSubscription: let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount) state = .waitingForSubscription(exp) case let .subscribed(subscription, _, elements): let maxFulfillmentCount = includingConsumed ? elements.count : elements.count - consumedCount let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount) expectation.fulfill(count: fulfillmentCount) let remainingCount = expectedFulfillmentCount - fulfillmentCount if remainingCount > 0 { let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount) state = .subscribed(subscription, exp, elements) } case .completed: expectation.fulfill(count: expectedFulfillmentCount) } } } /// Registers the expectation so that it gets fulfilled when /// publisher completes. func fulfillOnCompletion(_ expectation: XCTestExpectation) { synchronized { preconditionCanFulfillExpectation() switch state { case .waitingForSubscription: let exp = RecorderExpectation.onCompletion(expectation) state = .waitingForSubscription(exp) case let .subscribed(subscription, _, elements): let exp = RecorderExpectation.onCompletion(expectation) state = .subscribed(subscription, exp, elements) case .completed: expectation.fulfill() } } } /// Returns a value based on the recorded state of the publisher. /// /// - parameter value: A function which returns the value, given the /// recorded state of the publisher. /// - parameter elements: All recorded elements. /// - parameter completion: The eventual publisher completion. /// - parameter remainingElements: The elements that were not consumed yet. /// - parameter consume: A function which consumes elements. /// - parameter count: The number of consumed elements. /// - returns: The value func value(_ value: ( _ elements: [Input], _ completion: Subscribers.Completion?, _ remainingElements: ArraySlice, _ consume: (_ count: Int) -> ()) throws -> T) rethrows -> T { try synchronized { let (elements, completion) = state.elementsAndCompletion let remainingElements = elements[consumedCount...] return try value(elements, completion, remainingElements, { count in precondition(count >= 0) precondition(count <= remainingElements.count) consumedCount += count }) } } /// Checks that recorder can fulfill an expectation. /// /// The reason this method exists is that a recorder can fulfill a single /// expectation at a given time. It is a programmer error to wait for two /// expectations concurrently. /// /// This method MUST be called within a synchronized block. private func preconditionCanFulfillExpectation() { if let exp = state.recorderExpectation { // We are already waiting for an expectation! Is it a programmer // error? Recorder drops references to non-inverted expectations // when they are fulfilled. But inverted expectations are not // fulfilled, and thus not dropped. We can't quite know if an // inverted expectations has expired yet, so just let it go. precondition(exp.expectation.isInverted, "Already waiting for an expectation") } } // MARK: - Subscriber public func receive(subscription: Subscription) { synchronized { switch state { case let .waitingForSubscription(exp): state = .subscribed(subscription, exp, []) default: XCTFail("Publisher recorder is already subscribed") } } subscription.request(.unlimited) } public func receive(_ input: Input) -> Subscribers.Demand { return synchronized { switch state { case let .subscribed(subscription, exp, elements): var elements = elements elements.append(input) if case let .onInput(expectation, remainingCount: remainingCount) = exp { assert(remainingCount > 0) expectation.fulfill() if remainingCount > 1 { let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1) state = .subscribed(subscription, exp, elements) } else { state = .subscribed(subscription, nil, elements) } } else { state = .subscribed(subscription, exp, elements) } return .unlimited case .waitingForSubscription: XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))") return .none case .completed: XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))") return .none } } } public func receive(completion: Subscribers.Completion) { synchronized { switch state { case let .subscribed(_, exp, elements): if let exp = exp { switch exp { case let .onCompletion(expectation): expectation.fulfill() case let .onInput(expectation, remainingCount: remainingCount): expectation.fulfill(count: remainingCount) } } state = .completed(elements, completion) case .waitingForSubscription: XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))") case .completed: XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))") } } } } // MARK: - Publisher Expectations extension PublisherExpectations { /// The type of the publisher expectation returned by `Recorder.completion`. public typealias Completion = Map, Subscribers.Completion> /// The type of the publisher expectation returned by `Recorder.elements`. public typealias Elements = Map, [Input]> /// The type of the publisher expectation returned by `Recorder.last`. public typealias Last = Map, Input?> /// The type of the publisher expectation returned by `Recorder.single`. public typealias Single = Map, Input> } extension Recorder { /// Returns a publisher expectation which waits for the timeout to expire, /// or the recorded publisher to complete. /// /// When waiting for this expectation, the publisher error is thrown if /// the publisher fails before the expectation has expired. /// /// Otherwise, an array of all elements published before the expectation /// has expired is returned. /// /// Unlike other expectations, `availableElements` does not make a test fail /// on timeout expiration. It just returns the elements published so far. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testTimerPublishesIncreasingDates() throws { /// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() /// let recorder = publisher.record() /// let dates = try wait(for: recorder.availableElements, timeout: ...) /// XCTAssertEqual(dates.sorted(), dates) /// } public var availableElements: PublisherExpectations.AvailableElements { PublisherExpectations.AvailableElements(recorder: self) } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError.notCompleted is /// thrown if the publisher does not complete on time. /// /// Otherwise, a [Subscribers.Completion](https://developer.apple.com/documentation/combine/subscribers/completion) /// is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherCompletesWithSuccess() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let completion = try wait(for: recorder.completion, timeout: 1) /// if case let .failure(error) = completion { /// XCTFail("Unexpected error \(error)") /// } /// } public var completion: PublisherExpectations.Completion { recording.map { $0.completion } } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError.notCompleted is /// thrown if the publisher does not complete on time, and the publisher /// error is thrown if the publisher fails. /// /// Otherwise, an array of published elements is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherPublishesArrayElements() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) /// } public var elements: PublisherExpectations.Elements { recording.map { recording in if case let .failure(error) = recording.completion { throw error } return recording.output } } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, the publisher error is thrown if the /// publisher fails. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherFinishesWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// try wait(for: recorder.finished, timeout: 1) /// } /// /// This publisher expectation can be inverted: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectDoesNotFinish() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// try wait(for: recorder.finished.inverted, timeout: 1) /// } public var finished: PublisherExpectations.Finished { PublisherExpectations.Finished(recorder: self) } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError.notCompleted is /// thrown if the publisher does not complete on time, and the publisher /// error is thrown if the publisher fails. /// /// Otherwise, the last published element is returned, or nil if the publisher /// completes before it publishes any element. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherPublishesLastElementLast() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// if let element = try wait(for: recorder.last, timeout: 1) { /// XCTAssertEqual(element, "baz") /// } else { /// XCTFail("Expected one element") /// } /// } public var last: PublisherExpectations.Last { elements.map { $0.last } } /// Returns a publisher expectation which waits for the recorded publisher /// to emit one element, or to complete. /// /// When waiting for this expectation, a `RecordingError.notEnoughElements` /// is thrown if the publisher does not publish one element after last /// waited expectation. The publisher error is thrown if the publisher fails /// before publishing the next element. /// /// Otherwise, the next published element is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfTwoElementsPublishesElementsInOrder() throws { /// let publisher = ["foo", "bar"].publisher /// let recorder = publisher.record() /// /// var element = try wait(for: recorder.next(), timeout: 1) /// XCTAssertEqual(element, "foo") /// /// element = try wait(for: recorder.next(), timeout: 1) /// XCTAssertEqual(element, "bar") /// } public func next() -> PublisherExpectations.NextOne { PublisherExpectations.NextOne(recorder: self) } /// Returns a publisher expectation which waits for the recorded publisher /// to emit `count` elements, or to complete. /// /// When waiting for this expectation, a `RecordingError.notEnoughElements` /// is thrown if the publisher does not publish `count` elements after last /// waited expectation. The publisher error is thrown if the publisher fails /// before publishing the next `count` elements. /// /// Otherwise, an array of exactly `count` elements is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// /// var elements = try wait(for: recorder.next(2), timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// /// elements = try wait(for: recorder.next(1), timeout: 1) /// XCTAssertEqual(elements, ["baz"]) /// } /// /// - parameter count: The number of elements. public func next(_ count: Int) -> PublisherExpectations.Next { PublisherExpectations.Next(recorder: self, count: count) } /// Returns a publisher expectation which waits for the recorded publisher /// to emit `maxLength` elements, or to complete. /// /// When waiting for this expectation, the publisher error is thrown if the /// publisher fails before `maxLength` elements are published. /// /// Otherwise, an array of received elements is returned, containing at /// most `maxLength` elements, or less if the publisher completes early. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let elements = try wait(for: recorder.prefix(2), timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// } /// /// This publisher expectation can be inverted: /// /// // SUCCESS: no timeout, no error /// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { /// let publisher = PassthroughSubject() /// let recorder = publisher.record() /// publisher.send("foo") /// publisher.send("bar") /// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar"]) /// } /// /// - parameter maxLength: The maximum number of elements. public func prefix(_ maxLength: Int) -> PublisherExpectations.Prefix { PublisherExpectations.Prefix(recorder: self, maxLength: maxLength) } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError.notCompleted is /// thrown if the publisher does not complete on time. /// /// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording) /// is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testArrayPublisherRecording() throws { /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// let recording = try wait(for: recorder.recording, timeout: 1) /// XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) /// if case let .failure(error) = recording.completion { /// XCTFail("Unexpected error \(error)") /// } /// } public var recording: PublisherExpectations.Recording { PublisherExpectations.Recording(recorder: self) } /// Returns a publisher expectation which waits for the recorded publisher /// to complete. /// /// When waiting for this expectation, a RecordingError is thrown if the /// publisher does not complete on time, or does not publish exactly one /// element before it completes. The publisher error is thrown if the /// publisher fails. /// /// Otherwise, the single published element is returned. /// /// For example: /// /// // SUCCESS: no timeout, no error /// func testJustPublishesExactlyOneElement() throws { /// let publisher = Just("foo") /// let recorder = publisher.record() /// let element = try wait(for: recorder.single, timeout: 1) /// XCTAssertEqual(element, "foo") /// } public var single: PublisherExpectations.Single { elements.map { elements in guard let element = elements.first else { throw RecordingError.notEnoughElements } if elements.count > 1 { throw RecordingError.tooManyElements } return element } } } // MARK: - Publisher + Recorder extension Publisher { /// Returns a subscribed Recorder. /// /// For example: /// /// let publisher = ["foo", "bar", "baz"].publisher /// let recorder = publisher.record() /// /// You can build publisher expectations from the Recorder. For example: /// /// let elements = try wait(for: recorder.elements, timeout: 1) /// XCTAssertEqual(elements, ["foo", "bar", "baz"]) public func record() -> Recorder { let recorder = Recorder() subscribe(recorder) return recorder } } // MARK: - Convenience extension XCTestExpectation { fileprivate func fulfill(count: Int) { for _ in 0..() let recorder = publisher.record() _ = try wait(for: recorder.completion, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notCompleted { } } } // MARK: - Elements func testArrayPublisherSynchronouslyPublishesArrayElements() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try recorder.elements.get() XCTAssertEqual(elements, ["foo", "bar", "baz"]) } // SUCCESS: no timeout, no error func testArrayPublisherPublishesArrayElements() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 0.1) XCTAssertEqual(elements, ["foo", "bar", "baz"]) } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testElementsTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notCompleted { } } } // FAIL: Caught error MyError func testElementsSynchronousError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) _ = try recorder.elements.get() XCTFail("Expected error") } catch is MyError { } } // FAIL: Caught error MyError func testElementsError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.elements, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // MARK: - Finished // SUCCESS: no error func testArrayPublisherSynchronouslyFinishesWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() try recorder.finished.get() } // SUCCESS: no timeout, no error func testArrayPublisherFinishesWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() try wait(for: recorder.finished, timeout: 0.1) } // FAIL: Asynchronous wait failed func testFinishedTimeout() throws { try assertFailure("Asynchronous wait failed") { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.finished, timeout: 0.1) } } // FAIL: Caught error MyError func testFinishedError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // MARK: - Finished.inverted // SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotFinish() throws { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: 0.1) } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedFinishedError() throws { try assertFailure("Fulfilled inverted expectation") { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished.inverted, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } } // MARK: - Last // SUCCESS: no error func testArrayPublisherSynchronouslyPublishesLastElementLast() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() if let element = try recorder.last.get() { XCTAssertEqual(element, "baz") } else { XCTFail("Expected one element") } } // SUCCESS: no timeout, no error func testArrayPublisherPublishesLastElementLast() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() if let element = try wait(for: recorder.last, timeout: 0.1) { XCTAssertEqual(element, "baz") } else { XCTFail("Expected one element") } } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testLastTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notCompleted { } } } // FAIL: Caught error MyError func testLastError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.last, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // MARK: - next() // SUCCESS: no error func testPassthroughSubjectSynchronouslyPublishesElements() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") try XCTAssertEqual(recorder.next().get(), "foo") publisher.send("bar") try XCTAssertEqual(recorder.next().get(), "bar") } // SUCCESS: no error func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws { let publisher = ["foo", "bar"].publisher let recorder = publisher.record() var element = try recorder.next().get() XCTAssertEqual(element, "foo") element = try recorder.next().get() XCTAssertEqual(element, "bar") } // SUCCESS: no timeout, no error func testArrayOfTwoElementsPublishesElementsInOrder() throws { let publisher = ["foo", "bar"].publisher let recorder = publisher.record() var element = try wait(for: recorder.next(), timeout: 0.1) XCTAssertEqual(element, "foo") element = try wait(for: recorder.next(), timeout: 0.1) XCTAssertEqual(element, "bar") } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notEnoughElements { } } } // FAIL: Caught error MyError func testNextError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.next(), timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // FAIL: Caught error RecordingError.notEnoughElements func testNextNotEnoughElementsError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .finished) _ = try wait(for: recorder.next(), timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notEnoughElements { } } // MARK: - next().inverted // SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotPublishAnyElement() throws { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: 0.1) } // FAIL: Fulfilled inverted expectation func testInvertedNextTooEarly() throws { try assertFailure("Fulfilled inverted expectation") { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") try wait(for: recorder.next().inverted, timeout: 0.1) } } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedNextError() throws { try assertFailure("Fulfilled inverted expectation") { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.next().inverted, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } } // MARK: - next(count) // SUCCESS: no error func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() var elements = try recorder.next(2).get() XCTAssertEqual(elements, ["foo", "bar"]) elements = try recorder.next(1).get() XCTAssertEqual(elements, ["baz"]) } // SUCCESS: no timeout, no error func testArrayOfThreeElementsPublishesTwoThenOneElement() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() var elements = try wait(for: recorder.next(2), timeout: 0.1) XCTAssertEqual(elements, ["foo", "bar"]) elements = try wait(for: recorder.next(1), timeout: 0.1) XCTAssertEqual(elements, ["baz"]) } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextCountTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") _ = try wait(for: recorder.next(2), timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notEnoughElements { } } } // FAIL: Caught error MyError func testNextCountError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.next(2), timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // FAIL: Caught error RecordingError.notEnoughElements func testNextCountNotEnoughElementsError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .finished) _ = try wait(for: recorder.next(2), timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notEnoughElements { } } // MARK: - Prefix // SUCCESS: no error func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try recorder.prefix(2).get() XCTAssertEqual(elements, ["foo", "bar"]) } // SUCCESS: no timeout, no error func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 0.1) XCTAssertEqual(elements, ["foo", "bar"]) } // FAIL: Asynchronous wait failed func testPrefixTimeout() throws { try assertFailure("Asynchronous wait failed") { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") _ = try wait(for: recorder.prefix(2), timeout: 0.1) } } // FAIL: Caught error MyError func testPrefixError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.prefix(2), timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // MARK: - Prefix.inverted // SUCCESS: no timeout, no error func testPassthroughSubjectPublishesNoMoreThanSentValues() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) XCTAssertEqual(elements, ["foo", "bar"]) } // FAIL: Fulfilled inverted expectation func testInvertedPrefixTooEarly() throws { try assertFailure("Fulfilled inverted expectation") { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") publisher.send("baz") _ = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) } } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedPrefixError() throws { try assertFailure("Fulfilled inverted expectation") { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.prefix(3).inverted, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } } // MARK: - Recording // SUCCESS: no error func testArrayPublisherSynchronousRecording() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let recording = try recorder.recording.get() XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } // SUCCESS: no timeout, no error func testArrayPublisherRecording() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 0.1) XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testRecordingTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() _ = try wait(for: recorder.recording, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notCompleted { } } } // MARK: - Single // SUCCESS: no error func testJustSynchronouslyPublishesExactlyOneElement() throws { let publisher = Just("foo") let recorder = publisher.record() let element = try recorder.single.get() XCTAssertEqual(element, "foo") } // SUCCESS: no timeout, no error func testJustPublishesExactlyOneElement() throws { let publisher = Just("foo") let recorder = publisher.record() let element = try wait(for: recorder.single, timeout: 0.1) XCTAssertEqual(element, "foo") } // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testSingleTimeout() throws { try assertFailure("Asynchronous wait failed") { do { let publisher = PassthroughSubject() let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notCompleted { } } } // FAIL: Caught error MyError func testSingleError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) _ = try wait(for: recorder.single, timeout: 0.1) XCTFail("Expected error") } catch is MyError { } } // FAIL: Caught error RecordingError.tooManyElements func testSingleTooManyElementsError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") publisher.send(completion: .finished) _ = try wait(for: recorder.single, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.tooManyElements { } } // FAIL: Caught error RecordingError.notEnoughElements func testSingleNotEnoughElementsError() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(completion: .finished) _ = try wait(for: recorder.single, timeout: 0.1) XCTFail("Expected error") } catch RecordingError.notEnoughElements { } } } ================================================ FILE: Tests/CombineExpectationsTests/FailureTestCase.swift ================================================ import XCTest /// A XCTestCase subclass that can test its own failures. class FailureTestCase: XCTestCase { private struct Failure: Hashable { #if compiler(>=5.3) let issue: XCTIssue #else var description: String var file: String var line: Int var expected: Bool #endif #if compiler(>=5.3) func issue(prefix: String = "") -> XCTIssue { if prefix.isEmpty { return issue } else { return XCTIssue( type: issue.type, compactDescription: "\(prefix): \(issue.compactDescription)", detailedDescription: issue.detailedDescription, sourceCodeContext: issue.sourceCodeContext, associatedError: issue.associatedError, attachments: issue.attachments) } } #else func failure(prefix: String = "") -> (description: String, file: String, line: Int, expected: Bool) { let prefix = prefix.isEmpty ? "" : "\(prefix): " return ( description: prefix + description, file: file, line: line, expected: expected) } #endif #if compiler(>=5.3) private var description: String { return issue.compactDescription } #endif func hash(into hasher: inout Hasher) { hasher.combine(0) } static func == (lhs: Failure, rhs: Failure) -> Bool { lhs.description.hasPrefix(rhs.description) || rhs.description.hasPrefix(lhs.description) } } private var recordedFailures: [Failure] = [] private var isRecordingFailures = false func assertFailure(_ prefixes: String..., file: StaticString = #file, line: UInt = #line, _ execute: () throws -> Void) rethrows { let recordedFailures = try recordingFailures(execute) if prefixes.isEmpty { if recordedFailures.isEmpty { #if compiler(>=5.3) record(XCTIssue( type: .assertionFailure, compactDescription: "No failure did happen", detailedDescription: nil, sourceCodeContext: XCTSourceCodeContext( location: XCTSourceCodeLocation( filePath: String(describing: file), lineNumber: Int(line))), associatedError: nil, attachments: [])) #else recordFailure( withDescription: "No failure did happen", inFile: file.description, atLine: Int(line), expected: true) #endif } } else { let expectedFailures = prefixes.map { prefix -> Failure in #if compiler(>=5.3) return Failure(issue: XCTIssue( type: .assertionFailure, compactDescription: prefix, detailedDescription: nil, sourceCodeContext: XCTSourceCodeContext( location: XCTSourceCodeLocation( filePath: String(describing: file), lineNumber: Int(line))), associatedError: nil, attachments: [])) #else return Failure( description: prefix, file: String(describing: file), line: Int(line), expected: true) #endif } assertMatch( recordedFailures: recordedFailures, expectedFailures: expectedFailures) } } override func setUp() { super.setUp() isRecordingFailures = false recordedFailures = [] } #if compiler(>=5.3) override func record(_ issue: XCTIssue) { if isRecordingFailures { recordedFailures.append(Failure(issue: issue)) } else { super.record(issue) } } #else override func recordFailure(withDescription description: String, inFile filePath: String, atLine lineNumber: Int, expected: Bool) { if isRecordingFailures { recordedFailures.append(Failure( description: description, file: filePath, line: lineNumber, expected: expected)) } else { super.recordFailure( withDescription: description, inFile: filePath, atLine: lineNumber, expected: expected) } } #endif private func recordingFailures(_ execute: () throws -> Void) rethrows -> [Failure] { let oldRecordingFailures = isRecordingFailures let oldRecordedFailures = recordedFailures defer { isRecordingFailures = oldRecordingFailures recordedFailures = oldRecordedFailures } isRecordingFailures = true recordedFailures = [] try execute() let result = recordedFailures return result } private func assertMatch(recordedFailures: [Failure], expectedFailures: [Failure]) { let diff = expectedFailures.difference(from: recordedFailures).inferringMoves() for change in diff { switch change { case let .insert(offset: _, element: failure, associatedWith: nil): #if compiler(>=5.3) record(failure.issue(prefix: "Failure did not happen")) #else let failure = failure.failure(prefix: "Failure did not happen") recordFailure( withDescription: failure.description, inFile: failure.file, atLine: failure.line, expected: failure.expected) #endif case let .remove(offset: _, element: failure, associatedWith: nil): #if compiler(>=5.3) record(failure.issue()) #else let failure = failure.failure() recordFailure( withDescription: failure.description, inFile: failure.file, atLine: failure.line, expected: failure.expected) #endif default: break } } } } // MARK: - Tests class FailureTestCaseTests: FailureTestCase { func testEmptyTest() { } func testExpectedAnyFailure() { assertFailure { XCTFail("foo") } assertFailure { XCTFail("foo") XCTFail("bar") } } func testMissingAnyFailure() { assertFailure("No failure did happen") { assertFailure { } } } func testExpectedFailure() { assertFailure("failed - foo") { XCTFail("foo") } } func testExpectedFailureMatchesOnPrefix() { assertFailure("failed - foo") { XCTFail("foobarbaz") } } func testOrderOfExpectedFailureIsIgnored() { assertFailure("failed - foo", "failed - bar") { XCTFail("foo") XCTFail("bar") } assertFailure("failed - bar", "failed - foo") { XCTFail("foo") XCTFail("bar") } } func testExpectedFailureCanBeRepeated() { assertFailure("failed - foo", "failed - foo", "failed - bar") { XCTFail("foo") XCTFail("bar") XCTFail("foo") } } func testExactNumberOfRepetitionIsRequired() { assertFailure("Failure did not happen: failed - foo") { assertFailure("failed - foo", "failed - foo") { XCTFail("foo") } } assertFailure("failed - foo") { assertFailure("failed - foo", "failed - foo") { XCTFail("foo") XCTFail("foo") XCTFail("foo") } } } func testUnexpectedFailure() { assertFailure("Failure did not happen: failed - foo") { assertFailure("failed - foo") { } } } func testMissedFailure() { assertFailure("failed - bar") { assertFailure("failed - foo") { XCTFail("foo") XCTFail("bar") } } } } ================================================ FILE: Tests/CombineExpectationsTests/LateSubscriptionTest.swift ================================================ import XCTest import Combine import Foundation @testable import CombineExpectations /// Tests for subscribers that do not create subscriptions right when they /// receive subscribers. class LateSubscriptionTest: FailureTestCase { func testNoSubscriptionPublisher() throws { struct NoSubscriptionPublisher: Publisher { typealias Output = String typealias Failure = Never func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { } } do { let publisher = NoSubscriptionPublisher() let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertTrue(elements.isEmpty) XCTAssertNil(completion) } do { // a test with an expectation that is fulfilled on completion let publisher = NoSubscriptionPublisher() let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: 0.1) } do { // a test with an expectation that is fulfilled on input let publisher = NoSubscriptionPublisher() let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: 0.1) } } func testAsynchronousSubscriptionPublisher() throws { struct AsynchronousSubscriptionPublisher: Publisher { typealias Output = Base.Output typealias Failure = Base.Failure let base: Base func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { DispatchQueue.main.async { self.base.receive(subscriber: subscriber) } } } do { let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertTrue(elements.isEmpty) XCTAssertNil(completion) } do { // a test with an expectation that is fulfilled on completion let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 0.1) } do { // a test with an expectation that is fulfilled on input let publisher = AsynchronousSubscriptionPublisher(base: Just("foo")) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 0.1) XCTAssertEqual(element, "foo") let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, ["foo"]) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } } } ================================================ FILE: Tests/CombineExpectationsTests/RecorderTests.swift ================================================ import XCTest import Combine import Foundation @testable import CombineExpectations /// General tests for publisher expectations class RecorderTests: XCTestCase { private struct TestError: Error { } // MARK: - Subscription func testRecorderSubscribes() throws { var subscribed = false let publisher = Empty().handleEvents(receiveSubscription: { _ in subscribed = true }) _ = publisher.record() XCTAssertTrue(subscribed) } // MARK: - availableElements func testAvailableElementsSync() throws { do { let publisher = [1, 2, 3].publisher let recorder = publisher.record() let availableElements = try recorder.availableElements.get() XCTAssertEqual(availableElements, [1, 2, 3]) } do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(1) publisher.send(2) publisher.send(3) let availableElements = try recorder.availableElements.get() XCTAssertEqual(availableElements, [1, 2, 3]) } do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(1) publisher.send(completion: .failure(TestError())) _ = try recorder.availableElements.get() XCTFail("Expected TestError") } catch is TestError { } } func testAvailableElementsAsync() throws { do { let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect() let recorder = publisher.record() let dates = try wait(for: recorder.availableElements, timeout: 1) XCTAssertTrue(dates.count > 2) XCTAssertEqual(dates.sorted(), dates) } } func testAvailableElementsStopsOnPublisherCompletion() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { publisher.send(completion: .finished) } let start = Date() _ = try wait(for: recorder.availableElements, timeout: 2) let duration = Date().timeIntervalSince(start) XCTAssertLessThan(duration, 1) } } // MARK: - elementsAndCompletion func testElementsAndCompletionSync() throws { do { let publisher = Empty() let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } do { let publisher = (0..<1).publisher let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0]) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } do { let publisher = (0..<2).publisher let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0, 1]) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } } func testElementsAndCompletionAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0]) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0, 1]) if case let .failure(error) = try XCTUnwrap(completion) { throw error } } } func testElementsAndCompletionFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0]) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0, 1]) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } } func testElementsAndCompletionFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0]) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() var (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, []) XCTAssertNil(completion) _ = try wait(for: recorder.completion, timeout: 1) (elements, completion) = recorder.elementsAndCompletion XCTAssertEqual(elements, [0, 1]) if case .finished = try XCTUnwrap(completion) { XCTFail("Expected TestError") } } } // MARK: - wait(for: recorder.elements) func testWaitForElementsSync() throws { do { let publisher = Empty() let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForElementsAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForElementsFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForElementsFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.elements, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForElementsAndWaitAgain() throws { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0, 1]) do { let elements = try wait(for: recorder.elements, timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let elements = try wait(for: recorder.prefix(3), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let element = try wait(for: recorder.last, timeout: 1) XCTAssertEqual(element, 1) } do { _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.tooManyElements { } do { try wait(for: recorder.finished, timeout: 1) } do { let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } } // MARK: - wait(for: recorder.next()) func testWaitForNextSync() throws { do { let publisher = Empty() let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } } func testWaitForNextAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } } func testWaitForNextFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } } func testWaitForNextFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: 1) XCTAssertEqual(element, 0) } } func testWaitForNextInverted() throws { do { let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: 0.01) } do { let publisher = Timer.publish(every: 0.2, on: .main, in: .default).autoconnect() let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: 0.1) _ = try wait(for: recorder.next(), timeout: 1) } } func testNextNext() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) publisher.send(1) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) } do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 0) publisher.send(1) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 1) } do { let publisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() let recorder = publisher.record() _ = try wait(for: recorder.next(), timeout: 1) _ = try wait(for: recorder.next(), timeout: 1) } } func testPrefixNext() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) publisher.send(1) publisher.send(2) try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 2) try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) publisher.send(3) publisher.send(4) publisher.send(5) try XCTAssertEqual(wait(for: recorder.prefix(4), timeout: 1), [0, 1, 2, 3]) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 4) try XCTAssertEqual(wait(for: recorder.next(), timeout: 1), 5) } // MARK: - wait(for: recorder.next(0)) func testWaitForNext0Sync() throws { do { let publisher = Empty() let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } } func testWaitForNext0Async() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } } func testWaitForNext0Failure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } } func testWaitForNext0FailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } } // MARK: - wait(for: recorder.next(2)) func testWaitForNext2Sync() throws { do { let publisher = Empty() let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForNext2Async() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForNext2Failure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForNext2FailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.next(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testNext2Next2() throws { do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) publisher.send(1) publisher.send(2) publisher.send(3) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [0, 1]) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) } do { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) publisher.send(1) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [0, 1]) publisher.send(2) publisher.send(3) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) } do { let publisher = Timer.publish(every: 0.1, on: .main, in: .default).autoconnect() let recorder = publisher.record() _ = try wait(for: recorder.next(2), timeout: 1) _ = try wait(for: recorder.next(2), timeout: 1) } } func testPrefixNext2() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) publisher.send(1) publisher.send(2) publisher.send(3) try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [2, 3]) try XCTAssertEqual(wait(for: recorder.prefix(2), timeout: 1), [0, 1]) publisher.send(4) publisher.send(5) try XCTAssertEqual(wait(for: recorder.prefix(4), timeout: 1), [0, 1, 2, 3]) try XCTAssertEqual(wait(for: recorder.next(2), timeout: 1), [4, 5]) } // MARK: - wait(for: recorder.prefix(0)) func testWaitForPrefix0Sync() throws { do { let publisher = Empty() let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } } func testWaitForPrefix0Async() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } } func testWaitForPrefix0Failure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } } func testWaitForPrefix0FailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(0), timeout: 1) XCTAssertEqual(elements, []) } catch is TestError { } } // MARK: - wait(for: recorder.prefix(1)) func testWaitForPrefix1Sync() throws { do { let publisher = Empty() let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } } func testWaitForPrefix1Async() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } } func testWaitForPrefix1Failure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.prefix(1), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } catch is TestError { } } func testWaitForPrefix1FailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.prefix(1), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1), timeout: 1) XCTAssertEqual(elements, [0]) } catch is TestError { } } func testWaitForPrefix1Inverted() throws { do { let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(1).inverted, timeout: 0.01) XCTAssertEqual(elements, []) } } // MARK: - wait(for: recorder.prefix(2)) func testWaitForPrefix2Sync() throws { do { let publisher = Empty() let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForPrefix2Async() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } do { let publisher = (0..<3).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } } func testWaitForPrefix2Failure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.prefix(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.prefix(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } catch is TestError { } do { let publisher = (0..<3).publisher.append(error: TestError()) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } catch is TestError { } } func testWaitForPrefix2FailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.prefix(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.prefix(2), timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } catch is TestError { } do { let publisher = (0..<3).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2), timeout: 1) XCTAssertEqual(elements, [0, 1]) } catch is TestError { } } func testWaitForPrefix2Inverted() throws { do { let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2).inverted, timeout: 0.01) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(2).inverted, timeout: 0.01) XCTAssertEqual(elements, [0]) } } // MARK: - wait(for: recorder.prefix(3)) func testWaitForPrefix3Inverted() throws { do { let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.01) XCTAssertEqual(elements, []) } do { let publisher = (0..<1).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.01) XCTAssertEqual(elements, [0]) } do { let publisher = (0..<2).publisher.append(Empty().delay(for: 0.1, scheduler: DispatchQueue.main)) let recorder = publisher.record() let elements = try wait(for: recorder.prefix(3).inverted, timeout: 0.0) XCTAssertEqual(elements, [0, 1]) } } // MARK: - wait(for: recorder.prefix(N)) func testWaitForPrefixAndWaitForPrefixAgain() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) publisher.send(1) try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) publisher.send(2) try XCTAssertEqual(wait(for: recorder.prefix(3), timeout: 1), [0, 1, 2]) } func testWaitForPrefixAndWaitForPrefixAgainInverted() throws { let publisher = PassthroughSubject() let recorder = publisher.record() publisher.send(0) try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) try XCTAssertEqual(wait(for: recorder.prefix(1), timeout: 1), [0]) try XCTAssertEqual(wait(for: recorder.prefix(2).inverted, timeout: 0.01), [0]) } // MARK: - wait(for: recorder.last) func testWaitForLastSync() throws { do { let publisher = Empty() let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertNil(element) } do { let publisher = (0..<1).publisher let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertEqual(element, 1) } } func testWaitForLastAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertNil(element) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.last, timeout: 1) XCTAssertEqual(element, 1) } } func testWaitForLastFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForLastFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.last, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } // MARK: - wait(for: recorder.single) func testWaitForSingleSync() throws { do { let publisher = Empty() let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher let recorder = publisher.record() let element = try wait(for: recorder.single, timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.tooManyElements { } } func testWaitForSingleAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.notEnoughElements { } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let element = try wait(for: recorder.single, timeout: 1) XCTAssertEqual(element, 0) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected RecordingError") } catch RecordingError.tooManyElements { } } func testWaitForSingleFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForSingleFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() _ = try wait(for: recorder.single, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } // MARK: - wait(for: recorder.finished) func testWaitForFinishedSync() throws { do { let publisher = Empty() let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } do { let publisher = (0..<1).publisher let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } do { let publisher = (0..<2).publisher let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } } func testWaitForFinishedAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) } } func testWaitForFinishedFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForFinishedFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished, timeout: 1) XCTFail("Expected TestError") } catch is TestError { } } func testWaitForFinishedInverted() throws { do { let publisher = Empty().delay(for: 0.1, scheduler: DispatchQueue.main) let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: 0.01) } do { let publisher = PassthroughSubject() let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: 0.01) publisher.send(completion: .finished) try wait(for: recorder.finished, timeout: 0.01) } } // MARK: - wait(for: recorder.completion) func testWaitForCompletionSync() throws { do { let publisher = Empty() let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } do { let publisher = (0..<1).publisher let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } do { let publisher = (0..<2).publisher let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } } func testWaitForCompletionAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case let .failure(error) = completion { throw error } } } func testWaitForCompletionFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } } func testWaitForCompletionFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: 1) if case .finished = completion { XCTFail("Expected TestError") } } } // MARK: - wait(for: recorder.recording) func testWaitForRecordingSync() throws { do { let publisher = Empty() let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, []) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } do { let publisher = (0..<1).publisher let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } do { let publisher = (0..<2).publisher let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0, 1]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } } func testWaitForRecordingAsync() throws { do { let publisher = Empty().receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, []) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } do { let publisher = (0..<1).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } do { let publisher = (0..<2).publisher.receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0, 1]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } } func testWaitForRecordingFailure() throws { do { let publisher = Fail(error: TestError()) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, []) if case .finished = recording.completion { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0]) if case .finished = recording.completion { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0, 1]) if case .finished = recording.completion { XCTFail("Expected TestError") } } } func testWaitForRecordingFailureAsync() throws { do { let publisher = Fail(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, []) if case .finished = recording.completion { XCTFail("Expected TestError") } } do { let publisher = (0..<1).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0]) if case .finished = recording.completion { XCTFail("Expected TestError") } } do { let publisher = (0..<2).publisher.append(error: TestError()).receive(on: DispatchQueue.main) let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: 1) XCTAssertEqual(recording.output, [0, 1]) if case .finished = recording.completion { XCTFail("Expected TestError") } } } } ================================================ FILE: Tests/CombineExpectationsTests/Support.swift ================================================ import Combine extension Publisher where Failure == Never { /// Returns a publisher which completes with an error. func append(error: Failure) -> AnyPublisher { setFailureType(to: Failure.self) .append(Fail(error: error)) .eraseToAnyPublisher() } } ================================================ FILE: Tests/CombineExpectationsTests/WackySubscriberTests.swift ================================================ import XCTest import Combine import Foundation import CombineExpectations /// Tests that Recorder fail tests when they are fed with a subscriber that does /// not behave correctly, and messes with the Recorder state machine. /// /// Our goal is to make it clear that the problem with wacky publishers is /// wacky publishers, not this library. class WackySubscriberTests: FailureTestCase { func testDoubleSubscriptionPublisher() throws { struct DoubleSubscriptionPublisher: Publisher { typealias Output = Base.Output typealias Failure = Base.Failure let base: Base func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { base.receive(subscriber: subscriber) base.receive(subscriber: subscriber) } } assertFailure("failed - Publisher recorder is already subscribed") { let publisher = DoubleSubscriptionPublisher(base: Just("foo").makeConnectable()) _ = publisher.record() } } func testCompletionBeforeSubscriptionPublisher() throws { struct CompletionBeforeSubscriptionPublisher: Publisher { typealias Output = Never typealias Failure = Never func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { subscriber.receive(completion: .finished) } } assertFailure("failed - Publisher recorder got unexpected completion before subscription: finished") { let publisher = CompletionBeforeSubscriptionPublisher() _ = publisher.record() } } func testInputBeforeSubscriptionPublisher() throws { struct InputBeforeSubscriptionPublisher: Publisher { typealias Output = String typealias Failure = Never func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { _ = subscriber.receive("foo") } } assertFailure(#"failed - Publisher recorder got unexpected input before subscription: "foo""#) { let publisher = InputBeforeSubscriptionPublisher() _ = publisher.record() } } func testInputAfterCompletionPublisher() throws { struct InputAfterCompletionPublisher: Publisher where Base.Output == String { typealias Output = Base.Output typealias Failure = Base.Failure let base: Base func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { base.receive(subscriber: subscriber) _ = subscriber.receive("bar") } } assertFailure(#"failed - Publisher recorder got unexpected input after completion: "bar""#) { let publisher = InputAfterCompletionPublisher(base: Just("foo")) _ = publisher.record() } } func testDoubleCompletionPublisher() throws { struct DoubleCompletionPublisher: Publisher { typealias Output = Base.Output typealias Failure = Base.Failure let base: Base func receive(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input { base.receive(subscriber: subscriber) subscriber.receive(completion: .finished) } } assertFailure("failed - Publisher recorder got unexpected completion after completion") { let publisher = DoubleCompletionPublisher(base: Just("foo")) _ = publisher.record() } } } ================================================ FILE: Tests/CombineExpectationsTests/XCTestManifests.swift ================================================ import XCTest #if !canImport(ObjectiveC) public func allTests() -> [XCTestCaseEntry] { return [ testCase(CombineExpectationsTests.allTests), ] } #endif ================================================ FILE: Tests/LinuxMain.swift ================================================ import XCTest import CombineExpectationsTests var tests = [XCTestCaseEntry]() tests += CombineExpectationsTests.allTests() XCTMain(tests)