64 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 9cf67e3637 Prepare for release 0.13.0 2022-02-01 21:17:50 +03:00
Sergej Jaskiewicz 3877609ba2 fixup! Fix TSan false positives on Ubuntu 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 734e7e39cb Make async tests more reliable 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 2085bb7593 Fix tests on Xcode 10.3 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 5b247a5a01 Fix TSan false positives on Ubuntu 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 64f436c748 Disable TSan when testing with Xcode 10 and 13 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz b4e6313814 Fix some data races in tests 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz a0cf895c8c Don't generate LinuxMain.swift on newer Swift versions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz dec7d4a569 Bump swift-tools-version 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz fdc7550ff7 Fix SwiftLint, make it strict 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 135dc9a8ab Fix TSan tests on macOS 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 37a4fe400f Show GHA status in README 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 7b466153a6 Fix Swift 5.5 tests on Windows and Wasm 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz baac42a0ad Migrate macOS tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz fd05f5c8ff Migrate pod lib lint from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 8bfdcd4295 Migrate compatibility tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 90454807b4 WASM -> Wasm 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 77374fa820 Convert Windows GHA workflow to matrix 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 8eda9d7e3d Migrate Linux tests from CircleCI to GitHub Actions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 35cfe51c72 Generate LinuxMain.swift on Windows 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 42c0fa02ae Disable WASM tests on Swift 5.5
They don't compile due to presence of async test methods
2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 999a29cdf9 Support async tests in discover_tests.py 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 36edf4819b Run WASM tests with Swift 5.5 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz dfac8a9da7 Add manifest specifically for Swift 5.4 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 070ed94d18 Fix CI config 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz ea8938db72 Add tests for Publisher concurrency extensions, fix implementation 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4392b4610c Add tests for Future concurrency extensions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz c96f2e300d Update availability annotations for concurrency extensions 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 94de7bae46 Update the list of supported platforms 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz ed1b06ba51 Test with Swift 5.5.1 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4b2c87a0bb Update Future implementation 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 0243fd063d Enable concurrency only since Swift 5.5 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4fed5e9a5a Simplify a helper in Package.swift 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 80a4915715 Enable testing with Swift 5.4 on WASM 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 4716805f12 Make it compile 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 5490ff9be9 Enable testing with Swift 5.5 on Windows 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz c911862a24 [Xcode 13] Implement async/await support for publishers (no tests yet) 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz f823f7b18c Introduce take() helper method 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 02d1494ce9 [Xcode 13] Fix implementation so tests pass 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz f69bf6af64 [Xcode 13] Update tests so they pass in Combine compatibility mode 2022-02-01 18:58:40 +03:00
Sergej Jaskiewicz 866d837cdf [Xcode 13] Add new APIs to RemainingSwiftInterface.swift 2022-02-01 18:58:40 +03:00
Marcus Ficner 7d0a8cd6f8 Fix typo in Publishers.FlatMap.swift (#228) 2022-01-23 12:37:12 +00:00
Max Desiatov dfd3cdf890 Migrate SwiftLint checks from CircleCI to GHA (#226)
SwiftLint integration with Danger no longer works on CircleCI. I'm migrating it to GitHub Actions in a way that's known to work in other repositories.

* Create swiftlint.yml

* Update config.yml
2021-11-29 16:56:32 +01:00
ArthurChi ef0288e075 Implement Zip operator (#222)
Co-authored-by: Eric Patey <eric@patey.com>
Co-authored-by: Max Desiatov <max@desiatov.com>
Co-authored-by: ArthurChi <chijie@bytedance.com>
Co-Authored-By: Sergej Jaskiewicz <broadwaylamb@users.noreply.github.com>
2021-11-22 00:29:57 +01:00
Max Desiatov f219d6f6a5 Fix Slack invite link in README.md (#224)
Resolves #223.
2021-11-03 21:37:18 +01:00
Sergej Jaskiewicz 710dfa2715 Mention Windows in README.md 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 791625ff3b Disable running tests on iOS 9.3
CircleCI have deprecated the image :(
2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 7e4cdde419 "Fix" Publishers.Breakpoint tests on Windows 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 096e245d02 Support Windows threads in tests 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 1879860f35 I'm so tired of the Swift team breaking things on non-Darwin platforms
https://forums.swift.org/t/formalizing-the-unavailability-of-core-foundation/40216
2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz ace5778817 Support Windows in Package.swift 2021-09-24 16:26:09 +03:00
Sergej Jaskiewicz 12700a0500 Make COpenCombineHelpers buildable on Windows 2021-09-24 16:26:09 +03:00
Max Desiatov 6c8108f9dc Test with Windows on GitHub Actions 2021-09-24 16:26:09 +03:00
ArthurChi b27b2c31ce Subscribers reentrancy bugs fix (#211)
Co-authored-by: VassilyChi <chijie@bytedance.com>
2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz 3d3adb564b Release the Suffix publisher in Concatenate's Inner 2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz 925bee4af9 Fix reentrancy bugs in Subscribers.Assign 2021-07-29 01:48:45 +03:00
Sergej Jaskiewicz adcee8c14d Fix reentrancy bugs in Subscribers.Sink 2021-07-29 01:48:45 +03:00
dependabot[bot] 29126ac259 Bump addressable from 2.7.0 to 2.8.0 (#212) 2021-07-13 07:54:20 +00:00
Sergej Jaskiewicz bab8e08d2f Work around SwiftLint nested configuration bug
There is a bug introduced in SwiftLint 0.43.0 (?) when nested configurations don't work.
Nested configurations let us place additional .swiftlint.yml files in subdirectories that
specify rules that should only apply to that subdirectory. This is broken now.
2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 4060ee9f57 Fix compatibility with Xcode 12.5 toolchain and SDKs 2021-06-21 17:38:33 +03:00
Sergej Jaskiewicz 5996772433 Bump Xcode version for compatibility testing 2021-02-22 20:47:35 +03:00
Sergej Jaskiewicz cd45c77fac Implement Publishers.PrefixUntilOutput 2021-02-22 20:47:35 +03:00
Stuart Austin e618d179fe Add Publishers.Throttle implementation (#195)
* Publishers.Throttle implementation with tests

* Fix Throttle lint errors and removed expectation from throttle tests. Add additional test for cancelling a subscription before a scheduled value is emitted

* Fix VirtualTimeScheduler's executeSchedulesActions default deadline not being far enough into the future on 32-bit platforms.

* Fixed multiple lint errors

* Improve Publishers.Throttle code coverage by removing enum for pending emissions

* Additional Throttle test for cancelling a Subscriber when an output has been scheduled

* ThrottleTests now run on WASI
2021-02-18 13:56:55 +00:00
Marcus Scherer 4fa5f48c19 Fix typo (#204) 2021-02-08 19:41:49 +03:00
98 changed files with 6756 additions and 975 deletions
-297
View File
@@ -1,297 +0,0 @@
macOS_tests_steps: &macOS_tests_steps
steps:
- checkout
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-code-coverage --build-path .build-test-debug"
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: |
make test-debug-sanitize-thread \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-debug-sanitize-thread"
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Generating Xcode project
command: make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
- run:
name: Building for testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on macOS 10.15.0 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-sdk macosx10.15 \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
ubuntu_tests_steps: &ubuntu_tests_steps
steps:
- checkout
- run:
name: Installing dependencies
command: |
apt update -y
apt upgrade -y
apt install -y curl python3.8
- run:
name: "Generating LinuxMain.swift"
command: python3.8 utils/discover_tests.py
- run:
name: Building and running tests in debug mode with coverage
command: |
make test-debug \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--enable-code-coverage \
--disable-index-store \
--build-path .build-test-debug"
llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest \
> coverage.txt
- run:
name: Building and running tests in debug mode with TSan
command: |
make test-debug-sanitize-thread \
SWIFT_TEST_FLAGS="--disable-index-store \
--build-path .build-test-debug-sanitize-thread" \
- run:
name: Building and running tests in release mode
command: |
make test-release \
SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors" \
SWIFT_TEST_FLAGS="--build-path .build-test-release"
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash)
version: 2
jobs:
"Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)":
macos:
xcode: "11.3.0"
environment:
SWIFT_VERSION: "5.1.3"
<<: *macOS_tests_steps
"Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)":
macos:
xcode: "12.1.0"
environment:
SWIFT_VERSION: "5.3.0"
<<: *macOS_tests_steps
"Execute compatibility tests on iOS 14.2 (Xcode 12.2.0, Swift 5.3.1)":
macos:
xcode: "12.2.0"
environment:
SWIFT_VERSION: "5.3.1"
steps:
- checkout
- run:
name: Generating Xcode project
command: make generate-compatibility-xcodeproj
- run:
name: Building for testing on iOS 14.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.2" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing against Combine on iOS 14.2 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 11,OS=14.2" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
"Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)":
macos:
xcode: "10.2.1"
environment:
BUNDLE_PATH: .bundle # path to install gems and use for caching
SWIFT_VERSION: "5.0.1"
steps:
- checkout
- run:
name: Installing gem dependencies
command: bundle install && bundle clean
- restore_cache:
keys:
- v1-simulator-cache-{{ arch }}
- run:
# CircleCI doesn't have an iOS 9 simulator, so we need to install it manually.
name: Installing iOS 9 simulator
command: |
bundle exec xcversion simulators --install="iOS 9.3"
bundle exec xcversion simulators
xcrun simctl list
- save_cache:
key: v1-simulator-cache-{{ arch }}
paths:
- ~/Library/Caches/XcodeInstall
- run:
name: Generating Xcode project
command: |
make generate-xcodeproj SWIFT_BUILD_FLAGS="-Xswiftc -warnings-as-errors"
xcodebuild -scheme OpenCombine-Package -showdestinations
- run:
name: Building for testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild build-for-testing \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_build-for-testing.log \
| xcpretty
- store_artifacts:
path: xcodebuild_build-for-testing.log
- run:
name: Testing on iOS 9.3 with xcodebuild
command: |
set -o pipefail \
&& xcodebuild test-without-building \
-scheme OpenCombine-Package \
-destination "platform=iOS Simulator,name=iPhone 4s,OS=9.3" \
-derivedDataPath DerivedData \
| tee xcodebuild_test-without-building.log \
| xcpretty --report junit -o build/reports/results.xml
- store_artifacts:
path: xcodebuild_test-without-building.log
- store_test_results:
path: build/reports
- run:
name: Uploading code coverage
command: |
bash <(curl -s https://codecov.io/bash) -D DerivedData
"Execute tests on Ubuntu 18.04 (Swift 5.0)":
docker:
- image: swift:5.0-bionic
environment:
SWIFT_VERSION: "5.0"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.1)":
docker:
- image: swift:5.1-bionic
environment:
SWIFT_VERSION: "5.1"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.2)":
docker:
- image: swift:5.2-bionic
environment:
SWIFT_VERSION: "5.2"
<<: *ubuntu_tests_steps
"Execute tests on Ubuntu 18.04 (Swift 5.3)":
docker:
- image: swift:5.3-bionic
environment:
SWIFT_VERSION: "5.3"
<<: *ubuntu_tests_steps
"Run SwiftLint and Danger":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Install SwiftLint
command: |
brew install swiftlint
- run:
name: Install danger-swift
command: |
brew install danger/tap/danger-swift
- run:
name: Run danger-swift
command: danger-swift ci
"Run Pod spec lint":
macos:
xcode: "11.3.0"
environment:
HOMEBREW_NO_AUTO_UPDATE: "1"
steps:
- checkout
- run:
name: Pod lib lint
command: |
pod lib lint --allow-warnings --verbose
workflows:
version: 2
"OpenCombine: execute tests on macOS":
jobs:
- "Execute tests on macOS 10.15.0 (Xcode 11.3.0, Swift 5.1.3)"
- "Execute tests on macOS 10.15.0 (Xcode 12.1.0, Swift 5.3.0)"
"OpenCombine: execute compatibility tests":
jobs:
- "Execute compatibility tests on iOS 14.2 (Xcode 12.2.0, Swift 5.3.1)"
"OpenCombine: execute tests on iOS":
jobs:
- "Execute tests on iOS 9.3 (Xcode 10.2.1, Swift 5.0.1)"
"OpenCombine: execute tests on Linux":
jobs:
- "Execute tests on Ubuntu 18.04 (Swift 5.0)"
- "Execute tests on Ubuntu 18.04 (Swift 5.1)"
- "Execute tests on Ubuntu 18.04 (Swift 5.2)"
- "Execute tests on Ubuntu 18.04 (Swift 5.3)"
"OpenCombine: run SwiftLint and Danger":
jobs:
- "Run SwiftLint and Danger"
"OpenCombine: validate podspec files":
jobs:
- "Run Pod spec lint"
+16
View File
@@ -0,0 +1,16 @@
name: CocoaPods
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
validate_podspec:
name: Run pod lib lint
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run pod lib lint
run: pod lib lint --allow-warnings --verbose
+28
View File
@@ -0,0 +1,28 @@
name: Compatibility tests
on:
push:
branches: [master]
pull_request:
branches: [master]
schedule:
- cron: "0 9 * * 1" # Every Monday at 9:00 AM
jobs:
compatibility_tests_macos:
name: Execute compatibility tests
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- name: Run tests against Apple's Combine
# Attempt to run compatibility tests on macOS.
# If they fail, run on iOS.
run: |
make test-compatibility \
|| (set -o pipefail \
&& xcodebuild test \
-scheme OpenCombine-Package \
-destination "name=iPhone 13" \
-xcconfig Combine-Compatibility.xcconfig \
| tee xcodebuild_test.log \
| xcpretty)
+126
View File
@@ -0,0 +1,126 @@
name: macOS
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
# This job is not a part of the macos_test job because of
# the 'This copy of libswiftCore.dylib requires an OS version prior to 10.14.4.' error.
# We have to invoke install_name_tool and patch the test executable
# to work around this error.
#
# Other combinations of Xcode and macOS versions don't lead to this error.
swift_5_0_test:
name: Execute tests (macos-10.15, 10.3)
runs-on: macos-10.15
steps:
- uses: actions/checkout@v2
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "10.3"
- name: Swift version
run: swift --version
- name: Build and run tests in debug mode with coverage
run: |
swift build \
--build-tests \
-c debug \
-Xswiftc -warnings-as-errors \
-Xswiftc -profile-generate \
-Xswiftc -profile-coverage-mapping \
--build-path .build-test-debug
install_name_tool \
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
install_name_tool \
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
swift test \
--skip-build \
--enable-code-coverage \
--build-path .build-test-debug
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- name: Build and run tests in release mode
run: |
swift build \
--build-tests \
-c release \
-Xswiftc -warnings-as-errors \
-Xswiftc -profile-generate \
-Xswiftc -profile-coverage-mapping \
--build-path .build-test-release
install_name_tool \
-rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx /usr/lib/swift \
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
install_name_tool \
-add_rpath /Applications/Xcode_10.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx \
.build-test-release/release/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests
swift test \
--skip-build \
-c release \
--enable-code-coverage \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
macos_test:
name: Execute tests
strategy:
fail-fast: false
matrix:
include:
- os: macos-10.15
xcode-version: "11.3.1" # Swift 5.3.1
- os: macos-10.15
xcode-version: "11.7" # Swift 5.2.4
- os: macos-11
xcode-version: "12.4" # Swift 5.3.2
- os: macos-11
xcode-version: "12.5.1" # Swift 5.4.2
- os: macos-11
xcode-version: "13.2.1" # Swift 5.5.2
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: ${{ matrix.xcode-version }}
- name: Swift version
run: swift --version
- name: Build and run tests in debug mode with coverage
run: |
swift test \
-c debug \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-debug
xcrun llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest/Contents/MacOS/OpenCombinePackageTests \
> coverage.txt
- name: Build and run tests in debug mode with TSan
if: ${{ matrix.xcode-version != '13.2.1' }} # https://bugs.swift.org/browse/SR-15444
run: |
swift test \
-c debug \
--sanitize thread \
-Xswiftc -warnings-as-errors \
--build-path .build-test-debug-sanitize-thread
- name: Build and run tests in release mode
run: |
swift test \
-c release \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
+26
View File
@@ -0,0 +1,26 @@
name: SwiftLint
on:
pull_request:
paths:
- ".github/workflows/swiftlint.yml"
- ".swiftlint.yml"
- "**/*.swift"
jobs:
SwiftLint:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v1
# Fetch current versions of files
- name: Fetch base ref
run: |
git fetch --prune --no-tags --depth=1 origin +refs/heads/${{ github.base_ref }}:refs/heads/${{ github.base_ref }}
# Diff pull request to current files, then SwiftLint changed files
- name: GitHub Action for SwiftLint
uses: mayk-it/action-swiftlint@3.2.2
env:
DIFF_BASE: ${{ github.base_ref }}
DIFF_HEAD: HEAD
with:
args: --strict
+57
View File
@@ -0,0 +1,57 @@
name: Ubuntu
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
ubuntu_test:
name: Execute tests on Ubuntu
strategy:
fail-fast: false
matrix:
swift_version: ["5.0", "5.1", "5.2", "5.3", "5.4", "5.5"]
runs-on: ubuntu-latest
container: swift:${{ matrix.swift_version }}-bionic
steps:
- uses: actions/checkout@v2
- name: Generating LinuxMain.swift
if: >-
${{ matrix.swift_version == '5.0' ||
matrix.swift_version == '5.1' ||
matrix.swift_version == '5.2' ||
matrix.swift_version == '5.3' }}
run: |
apt update -y
apt upgrade -y
apt install -y python3.8
python3.8 utils/discover_tests.py
- name: Building and running tests in debug mode with coverage
run: |
swift test \
-c debug \
-Xswiftc -warnings-as-errors \
--enable-code-coverage \
--build-path .build-test-debug
llvm-cov show \
-instr-profile=.build-test-debug/debug/codecov/default.profdata \
.build-test-debug/debug/OpenCombinePackageTests.xctest \
> coverage.txt
- name: Building and running tests in debug mode with TSan
if: ${{ matrix.swift_version != '5.0' }} # There are false positives there
run: |
swift test \
-c debug \
--sanitize thread \
--build-path .build-test-debug-sanitize-thread
- name: Building and running tests in release mode
run: |
swift test \
-c release \
-Xswiftc -warnings-as-errors \
--build-path .build-test-release
- uses: codecov/codecov-action@v2
with:
verbose: true
+19 -2
View File
@@ -1,4 +1,4 @@
name: SwiftWasm
name: Wasm
on:
push:
@@ -7,10 +7,27 @@ on:
branches: [master]
jobs:
carton_wasmer_test:
carton_wasmer_test_5_3:
name: "Execute tests on Wasm (Swift 5.3)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.3
carton_wasmer_test_5_4:
name: "Execute tests on Wasm (Swift 5.4)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.4
carton_wasmer_test_5_5:
name: "Execute tests on Wasm (Swift 5.5)"
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: swiftwasm/swiftwasm-action@v5.5
+21
View File
@@ -0,0 +1,21 @@
name: Windows
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
windows_test:
name: Execute tests on Windows
strategy:
fail-fast: false
matrix:
swift_version: ["5.4.2", "5.5.1"]
runs-on: windows-2019
steps:
- uses: actions/checkout@v2
- uses: MaxDesiatov/swift-windows-action@v1
with:
swift-version: ${{ matrix.swift_version }}
+2
View File
@@ -2,6 +2,8 @@ included:
- Sources
- Tests
child_config: Tests/.swiftlint.yml
disabled_rules:
- block_based_kvo
- class_delegate_protocol
+3 -3
View File
@@ -66,10 +66,10 @@ do {
}
}
SwiftLint.lint(inline: true,
SwiftLint.lint(.all(directory: nil),
inline: true,
configFile: ".swiftlint.yml",
strict: true,
lintAllFiles: true)
strict: true)
if danger.warnings.isEmpty, danger.fails.isEmpty {
markdown("LGTM")
+1 -1
View File
@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.1)
addressable (2.7.0)
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.3)
+3 -3
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombine"
spec.version = "0.12.0"
spec.version = "0.13.0"
spec.summary = "Open source implementation of Apple's Combine framework for processing values over time."
spec.description = <<-DESC
An open source implementation of Apple's Combine framework for processing values over time.
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
+4 -4
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineDispatch"
spec.version = "0.12.0"
spec.version = "0.13.0"
spec.summary = "OpenCombine + Dispatch interoperability"
spec.description = <<-DESC
Extends `DispatchQueue` with conformance to the `Scheduler` protocol
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineDispatch/**/*.swift"
spec.dependency "OpenCombine", '>= 0.10.2'
spec.dependency "OpenCombine", '>= 0.12.0'
end
+4 -4
View File
@@ -1,17 +1,17 @@
Pod::Spec.new do |spec|
spec.name = "OpenCombineFoundation"
spec.version = "0.12.0"
spec.version = "0.13.0"
spec.summary = "OpenCombine + OpenCombineFoundation interoperability"
spec.description = <<-DESC
Adds publishers to Foundation types like NotificationCenter, URLSession etc.
DESC
spec.homepage = "https://github.com/broadwaylamb/OpenCombine/"
spec.homepage = "https://github.com/OpenCombine/OpenCombine/"
spec.license = "MIT"
spec.authors = { "Sergej Jaskiewicz" => "jaskiewiczs@icloud.com" }
spec.source = { :git => "https://github.com/broadwaylamb/OpenCombine.git", :tag => "#{spec.version}" }
spec.source = { :git => "https://github.com/OpenCombine/OpenCombine.git", :tag => "#{spec.version}" }
spec.swift_version = "5.0"
@@ -21,5 +21,5 @@ Pod::Spec.new do |spec|
spec.tvos.deployment_target = "9.0"
spec.source_files = "Sources/OpenCombineFoundation/**/*.swift"
spec.dependency "OpenCombine", '>= 0.10.2'
spec.dependency "OpenCombine", '>= 0.12.0'
end
+7 -9
View File
@@ -1,4 +1,4 @@
// swift-tools-version:5.3
// swift-tools-version:5.5
import PackageDescription
@@ -6,13 +6,14 @@ import PackageDescription
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.macCatalyst,
.iOS,
.watchOS,
.tvOS,
.driverKit,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.windows,
.wasi,
]
@@ -33,6 +34,7 @@ let package = Package(
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
@@ -73,17 +75,13 @@ let package = Package(
]
)
],
cxxLanguageStandard: .cxx1z
cxxLanguageStandard: .cxx17
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
return filter { !exceptions.contains($0) }
}
}
+90
View File
@@ -0,0 +1,90 @@
// swift-tools-version:5.3
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
// Disable Windows because of https://bugs.swift.org/browse/SR-13817
// .windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx1z
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
// See: https://bugs.swift.org/browse/SR-13813
let exceptionsDescriptions = exceptions.map(String.init(describing:))
return filter { platform in
!exceptionsDescriptions.contains(String(describing: platform))
}
}
}
+85
View File
@@ -0,0 +1,85 @@
// swift-tools-version:5.4
import PackageDescription
// This list should be updated whenever SwiftPM adds support for a new platform.
// See: https://bugs.swift.org/browse/SR-13814
let supportedPlatforms: [Platform] = [
.macOS,
.iOS,
.watchOS,
.tvOS,
.linux,
.android,
.windows,
.wasi,
]
let package = Package(
name: "OpenCombine",
products: [
.library(name: "OpenCombine", targets: ["OpenCombine"]),
.library(name: "OpenCombineDispatch", targets: ["OpenCombineDispatch"]),
.library(name: "OpenCombineFoundation", targets: ["OpenCombineFoundation"]),
.library(name: "OpenCombineShim", targets: ["OpenCombineShim"]),
],
targets: [
.target(name: "COpenCombineHelpers"),
.target(
name: "OpenCombine",
dependencies: [
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
],
exclude: [
"Concurrency/Publisher+Concurrency.swift.gyb",
"Publishers/Publishers.Encode.swift.gyb",
"Publishers/Publishers.MapKeyPath.swift.gyb",
"Publishers/Publishers.Catch.swift.gyb"
],
swiftSettings: [.define("WASI", .when(platforms: [.wasi]))]
),
.target(name: "OpenCombineDispatch", dependencies: ["OpenCombine"]),
.target(
name: "OpenCombineFoundation",
dependencies: [
"OpenCombine",
.target(name: "COpenCombineHelpers",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.target(
name: "OpenCombineShim",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi])))
]
),
.testTarget(
name: "OpenCombineTests",
dependencies: [
"OpenCombine",
.target(name: "OpenCombineDispatch",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
.target(name: "OpenCombineFoundation",
condition: .when(platforms: supportedPlatforms.except([.wasi]))),
],
swiftSettings: [
.unsafeFlags(["-enable-testing"]),
.define("WASI", .when(platforms: [.wasi]))
]
)
],
cxxLanguageStandard: .cxx17
)
// MARK: Helpers
extension Array where Element == Platform {
func except(_ exceptions: [Platform]) -> [Platform] {
return filter { !exceptions.contains($0) }
}
}
+15 -8
View File
@@ -1,14 +1,21 @@
# OpenCombine
[![OpenCombine](https://circleci.com/gh/OpenCombine/OpenCombine.svg?style=svg)](https://circleci.com/gh/OpenCombine/OpenCombine)
[![codecov](https://codecov.io/gh/OpenCombine/OpenCombine/branch/master/graph/badge.svg)](https://codecov.io/gh/OpenCombine/OpenCombine)
![Language](https://img.shields.io/badge/Swift-5.0-orange.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS%20%7C%20iOS%20%7C%20watchOS%20%7C%20tvOS%20%7C%20Wasm-lightgrey.svg)
![Cocoapods](https://img.shields.io/cocoapods/v/OpenCombine?color=blue)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/enQtNzE2MjE5NzkxODI0LTYxMjkzNDUxZWViZWI1Njc2YjBhODgxNjRjOTdkZTcxOGU2ZjJjZjYxMGI3NWZkN2RkNGFmZTUzNmU3MGE2ZWM)
[<img src="https://img.shields.io/badge/slack-OpenCombine-yellow.svg?logo=slack">](https://join.slack.com/t/opencombine/shared_invite/zt-96rr6cyf-0Hk5_hY8nM5zta6M56Jfzg)
Open-source implementation of Apple's [Combine](https://developer.apple.com/documentation/combine) framework for processing values over time.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux and WebAssembly.
The main goal of this project is to provide a compatible, reliable and efficient implementation which can be used on Apple's operating systems before macOS 10.15 and iOS 13, as well as Linux, Windows and WebAssembly.
| **CI Status** |
|---|
|[![Compatibility tests](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml)|
|[![macOS](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml)|
|[![Ubuntu](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml)|
|[![Windows](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml)|
|[![Wasm](https://github.com/OpenCombine/OpenCombine/actions/workflows/wasm.yml/badge.svg)](https://github.com/OpenCombine/OpenCombine/actions/workflows/wasm.yml)|
### Installation
`OpenCombine` contains three public targets: `OpenCombine`, `OpenCombineFoundation` and `OpenCombineDispatch` (the fourth one, `COpenCombineHelpers`, is considered private. Don't import it in your projects).
@@ -26,7 +33,7 @@ To add `OpenCombine` to your [SwiftPM](https://swift.org/package-manager/) packa
```swift
dependencies: [
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.12.0")
.package(url: "https://github.com/OpenCombine/OpenCombine.git", from: "0.13.0")
],
targets: [
.target(
@@ -54,9 +61,9 @@ To do so, open Xcode, use **File** → **Swift Packages** → **Add Package Depe
To add `OpenCombine` to a project using [CocoaPods](https://cocoapods.org/), add `OpenCombine` and `OpenCombineDispatch` to the list of target dependencies in your `Podfile`.
```ruby
pod 'OpenCombine', '~> 0.12.0'
pod 'OpenCombineDispatch', '~> 0.12.0'
pod 'OpenCombineFoundation', '~> 0.12.0'
pod 'OpenCombine', '~> 0.13.0'
pod 'OpenCombineDispatch', '~> 0.13.0'
pod 'OpenCombineFoundation', '~> 0.13.0'
```
### Contributing
+58 -204
View File
@@ -244,47 +244,6 @@ extension Publisher {
public func collect<S>(_ strategy: Publishers.TimeGroupingStrategy<S>, options: S.SchedulerOptions? = nil) -> Publishers.CollectByTime<Self, S> where S : Scheduler
}
extension Publishers {
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
public init(upstream: Upstream, other: Other)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher publishes an element.
public func prefix<P>(untilOutputFrom publisher: P) -> Publishers.PrefixUntilOutput<Self, P> where P : Publisher
}
extension Publishers {
/// A publisher created by applying the merge function to two upstream publishers.
@@ -675,6 +634,64 @@ extension Publisher {
public func merge(with other: Self) -> Publishers.MergeMany<Self>
}
extension Publishers {
/// A publisher that attempts to recreate its subscription to a failed upstream publisher.
public struct Retry<Upstream> : Publisher where Upstream : Publisher {
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The maximum number of retry attempts to perform.
///
/// If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public let retries: Int?
/// Creates a publisher that attempts to recreate its subscription to a failed upstream publisher.
///
/// - Parameters:
/// - upstream: The publisher from which this publisher receives its elements.
/// - retries: The maximum number of retry attempts to perform. If `nil`, this publisher attempts to reconnect with the upstream publisher an unlimited number of times.
public init(upstream: Upstream, retries: Int?)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publisher {
/// Attempts to recreate a failed subscription with the upstream publisher using a specified number of attempts to establish the connection.
///
/// After exceeding the specified number of retries, the publisher passes the failure to the downstream receiver.
/// - Parameter retries: The number of times to attempt to recreate the subscription.
/// - Returns: A publisher that attempts to recreate its subscription to a failed upstream publisher.
public func retry(_ retries: Int) -> Publishers.Retry<Self>
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by the upstream publisher in a specified time interval.
@@ -758,169 +775,6 @@ extension Publisher {
public func throttle<S>(for interval: S.SchedulerTimeType.Stride, scheduler: S, latest: Bool) -> Publishers.Throttle<Self, S> where S : Scheduler
}
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
public struct Zip<A, B> : Publisher where A : Publisher, B : Publisher, A.Failure == B.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public init(_ a: A, _ b: B)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, B.Failure == S.Failure, S.Input == (A.Output, B.Output)
}
/// A publisher created by applying the zip function to three upstream publishers.
public struct Zip3<A, B, C> : Publisher where A : Publisher, B : Publisher, C : Publisher, A.Failure == B.Failure, B.Failure == C.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public init(_ a: A, _ b: B, _ c: C)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, C.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output)
}
/// A publisher created by applying the zip function to four upstream publishers.
public struct Zip4<A, B, C, D> : Publisher where A : Publisher, B : Publisher, C : Publisher, D : Publisher, A.Failure == B.Failure, B.Failure == C.Failure, C.Failure == D.Failure {
/// The kind of values published by this publisher.
public typealias Output = (A.Output, B.Output, C.Output, D.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = A.Failure
public let a: A
public let b: B
public let c: C
public let d: D
public init(_ a: A, _ b: B, _ c: C, _ d: D)
/// This function is called to attach the specified `Subscriber` to this `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<S>(subscriber: S) where S : Subscriber, D.Failure == S.Failure, S.Input == (A.Output, B.Output, C.Output, D.Output)
}
}
extension Publisher {
/// Combine elements from another publisher and deliver pairs of elements as tuples.
///
/// The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
public func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure
/// Combine elements from another publisher and deliver a transformed output.
///
/// The returned publisher waits until both publishers have emitted an event, then delivers the oldest unconsumed event from each publisher together as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers as tuples.
public func zip<P, T>(_ other: P, _ transform: @escaping (Self.Output, P.Output) -> T) -> Publishers.Map<Publishers.Zip<Self, P>, T> where P : Publisher, Self.Failure == P.Failure
/// Combine elements from two other publishers and deliver groups of elements as tuples.
///
/// The returned publisher waits until all three publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q>(_ publisher1: P, _ publisher2: Q) -> Publishers.Zip3<Self, P, Q> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Combine elements from two other publishers and deliver a transformed output.
///
/// The returned publisher waits until all three publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, T>(_ publisher1: P, _ publisher2: Q, _ transform: @escaping (Self.Output, P.Output, Q.Output) -> T) -> Publishers.Map<Publishers.Zip3<Self, P, Q>, T> where P : Publisher, Q : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure
/// Combine elements from three other publishers and deliver groups of elements as tuples.
///
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.Zip4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
/// Combine elements from three other publishers and deliver a transformed output.
///
/// The returned publisher waits until all four publishers have emitted an event, then delivers the oldest unconsumed event from each publisher as a tuple to the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2` emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and publisher `P4` emits the event `g`, the zip publisher emits the tuple `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - transform: A closure that receives the most recent value from each publisher and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers as tuples.
public func zip<P, Q, R, T>(_ publisher1: P, _ publisher2: Q, _ publisher3: R, _ transform: @escaping (Self.Output, P.Output, Q.Output, R.Output) -> T) -> Publishers.Map<Publishers.Zip4<Self, P, Q, R>, T> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure
}
extension Publishers.CombineLatest : Equatable where A : Equatable, B : Equatable {
/// Returns a Boolean value that indicates whether two publishers are equivalent.
@@ -10,13 +10,31 @@
#include <atomic>
#include <cstdlib>
#include <system_error>
#include <pthread.h>
#include <signal.h>
#if __has_include(<pthread.h>)
# include <pthread.h>
# define OPENCOMBINE_HAS_PTHREAD 1
#else
# define OPENCOMBINE_HAS_PTHREAD 0
#endif
#if __has_include(<signal.h>)
# include <signal.h>
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 1
#else
# define OPENCOMBINE_HAS_SIGNAL_HANDLING 0
#endif
#ifdef _WIN32
# include <windows.h>
#endif
#ifdef __APPLE__
#include <os/lock.h>
#endif // __APPLE__
#include <mutex>
// Throwing exceptions through language boundaries is undefined behavior,
// so we must catch all of them in our extern "C" functions.
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
@@ -47,10 +65,11 @@ public:
virtual void unlock() = 0;
virtual void assertOwner() {}
virtual ~PlatformIndependentMutex() noexcept(false) {}
virtual ~PlatformIndependentMutex() {}
};
class PThreadMutex : PlatformIndependentMutex {
#if OPENCOMBINE_HAS_PTHREAD
class PThreadMutex final : PlatformIndependentMutex {
private:
pthread_mutex_t mutex_;
public:
@@ -66,20 +85,16 @@ public:
PThreadMutex(PThreadMutex&&) = delete;
PThreadMutex& operator=(PThreadMutex&&) = delete;
void lock() override final {
void lock() override {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_lock(&mutex_));
}
void unlock() override final {
void unlock() override {
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_unlock(&mutex_));
}
~PThreadMutex() {
// Yep, this destructor may throw. This is deliberate, since pthread_mutex_destroy
// may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_destroy(&mutex_));
pthread_mutex_destroy(&mutex_);
}
protected:
class Attributes {
@@ -107,12 +122,8 @@ protected:
setType(PTHREAD_MUTEX_ERRORCHECK);
}
~Attributes() noexcept(false) {
// Yep, this destructor may throw. This is deliberate,
// since pthread_mutexattr_destroy may fail.
//
// The altrenative is to just silently ignore the error, which is even worse.
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutexattr_destroy(&attrs_));
~Attributes() {
pthread_mutexattr_destroy(&attrs_);
}
private:
void setType(int type) {
@@ -124,21 +135,7 @@ protected:
OPENCOMBINE_HANDLE_PTHREAD_CALL(pthread_mutex_init(&mutex_, attributes.raw()));
}
};
class PThreadRecursiveMutex final : PThreadMutex {
public:
PThreadRecursiveMutex() {
Attributes attrs;
attrs.setRecursive();
initialize(attrs);
}
PThreadRecursiveMutex(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex& operator=(const PThreadRecursiveMutex&) = delete;
PThreadRecursiveMutex(PThreadRecursiveMutex&&) = delete;
PThreadRecursiveMutex& operator=(PThreadRecursiveMutex&&) = delete;
};
#endif // OPENCOMBINE_HAS_PTHREAD
#ifdef __APPLE__
@@ -167,9 +164,32 @@ public:
};
#endif // __APPLE__
} // end anonymous namespace
template <typename Mu>
class GenericMutex final : PlatformIndependentMutex {
Mu mutex_;
public:
extern "C" {
GenericMutex() = default;
GenericMutex(const GenericMutex&) = delete;
GenericMutex& operator=(const GenericMutex&) = delete;
GenericMutex(GenericMutex&&) = delete;
GenericMutex& operator=(GenericMutex&&) = delete;
void lock() override {
mutex_.lock();
}
void unlock() override {
mutex_.unlock();
}
};
using StdMutex = GenericMutex<std::mutex>;
using StdRecursiveMutex = GenericMutex<std::recursive_mutex>;
} // end anonymous namespace
uint64_t opencombine_next_combine_identifier(void) {
return next_combine_identifier.fetch_add(1);
@@ -177,24 +197,27 @@ uint64_t opencombine_next_combine_identifier(void) {
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
#ifdef __APPLE__
if (__builtin_available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)) {
return {new OSUnfairLock};
} else {
return {new PThreadMutex};
}
#else
#elif OPENCOMBINE_HAS_PTHREAD
// When possible, use pthread mutex implementation, because it allows
// setting the PTHREAD_MUTEX_ERRORCHECK attribute, which makes
// recursive locking a hard error instead of UB.
return {new PThreadMutex};
#else
return {new StdMutex};
#endif
OPENCOMBINE_HANDLE_EXCEPTION_END
}
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void) {
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
return {new PThreadRecursiveMutex};
return {new StdRecursiveMutex};
OPENCOMBINE_HANDLE_EXCEPTION_END
}
@@ -237,7 +260,9 @@ void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lo
}
void opencombine_stop_in_debugger(void) {
#if _WIN32
DebugBreak();
#elif OPENCOMBINE_HAS_SIGNAL_HANDLING
raise(SIGTRAP);
#endif
}
} // extern "C"
+1 -1
View File
@@ -9,7 +9,7 @@ extension Publisher {
/// Wraps this publisher with a type eraser.
///
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublishe`` to
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher`` to
/// the downstream subscriber, rather than this publishers actual type.
/// This form of _type erasure_ preserves abstraction across API boundaries, such as
/// different modules.
@@ -0,0 +1,125 @@
//
// Future+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Future where Failure == Never {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var value: Output {
get async {
await ContinuationSubscriber.withUnsafeSubscription(self)
}
}
}
extension Future {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var value: Output {
get async throws {
try await ContinuationSubscriber.withUnsafeThrowingSubscription(self)
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
private final class ContinuationSubscriber<Input,
UpstreamFailure: Error,
ErrorOrNever: Error>
: Subscriber
{
typealias Failure = UpstreamFailure
private var continuation: UnsafeContinuation<Input, ErrorOrNever>?
private var subscription: Subscription?
private let lock = UnfairLock.allocate()
private init(_ continuation: UnsafeContinuation<Input, ErrorOrNever>) {
self.continuation = continuation
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard self.subscription == nil else {
assertionFailure("Unexpected state: received subscription twice")
lock.unlock()
subscription.cancel()
return
}
self.subscription = subscription
lock.unlock()
subscription.request(.max(1))
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
if let continuation = self.continuation.take() {
lock.unlock()
continuation.resume(returning: input)
} else {
assertionFailure("Unexpected state: already completed")
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
subscription = nil
lock.unlock()
completion.failure.map(handleFailure)
}
private func handleFailure(_ error: Failure) {
lock.lock()
if let continuation = self.continuation.take() {
lock.unlock()
continuation.resume(throwing: error as! ErrorOrNever)
} else {
assertionFailure("Unexpected state: already completed")
lock.unlock()
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ContinuationSubscriber where ErrorOrNever == Error {
fileprivate static func withUnsafeThrowingSubscription<Upstream: Publisher>(
_ upstream: Upstream
) async throws -> Input
where Upstream.Output == Input,
Upstream.Failure == UpstreamFailure
{
try await withUnsafeThrowingContinuation { continuation in
upstream.subscribe(ContinuationSubscriber(continuation))
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ContinuationSubscriber where UpstreamFailure == Never, ErrorOrNever == Never {
fileprivate static func withUnsafeSubscription<Upstream: Publisher>(
_ upstream: Upstream
) async -> Input
where Upstream.Output == Input,
Upstream.Failure == Never
{
await withUnsafeContinuation { continuation in
upstream.subscribe(ContinuationSubscriber(continuation))
}
}
}
#endif
@@ -0,0 +1,331 @@
//
//
// Auto-generated from GYB template. DO NOT EDIT!
//
//
//
//
// Publisher+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Publisher where Failure == Never {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: AsyncPublisher<Self> {
return .init(self)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct AsyncPublisher<Upstream: Publisher>: AsyncSequence
where Upstream.Failure == Never
{
public typealias Element = Upstream.Output
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
public mutating func next() async -> Element? {
return await withTaskCancellationHandler(
handler: { [inner] in inner.cancel() },
operation: { [inner] in await inner.next() }
)
}
}
public typealias AsyncIterator = Iterator
private let publisher: Upstream
public init(_ publisher: Upstream) {
self.publisher = publisher
}
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension AsyncPublisher.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, Never>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
state = .terminal
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async -> Input? {
return await withUnsafeContinuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal:
lock.unlock()
continuation.resume(returning: nil)
}
}
}
}
}
extension Publisher {
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: AsyncThrowingPublisher<Self> {
return .init(self)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct AsyncThrowingPublisher<Upstream: Publisher>: AsyncSequence
{
public typealias Element = Upstream.Output
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
public mutating func next() async throws -> Element? {
return try await withTaskCancellationHandler(
handler: { [inner] in inner.cancel() },
operation: { [inner] in try await inner.next() }
)
}
}
public typealias AsyncIterator = Iterator
private let publisher: Upstream
public init(_ publisher: Upstream) {
self.publisher = publisher
}
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension AsyncThrowingPublisher.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal(Error?)
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, Error>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
switch state {
case .awaitingSubscription, .subscribed:
if let continuation = pending.first {
state = .terminal(nil)
let remaining = pending.take().dropFirst()
lock.unlock()
switch completion {
case .finished:
continuation.resume(returning: nil)
case .failure(let error):
continuation.resume(throwing: error)
}
remaining.resumeAllWithNil()
} else {
state = .terminal(completion.failure)
lock.unlock()
}
case .terminal:
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal(nil)
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal(nil)
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async throws -> Input? {
return try await withUnsafeThrowingContinuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal(nil):
lock.unlock()
continuation.resume(returning: nil)
case .terminal(let error?):
state = .terminal(nil)
lock.unlock()
continuation.resume(throwing: error)
}
}
}
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Sequence {
fileprivate func resumeAllWithNil<Output, Failure: Error>()
where Element == UnsafeContinuation<Output?, Failure>
{
for continuation in self {
continuation.resume(returning: nil)
}
}
}
#endif
@@ -0,0 +1,203 @@
${template_header}
//
// Publisher+Concurrency.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
%{
instantiations = [('AsyncPublisher', False), ('AsyncThrowingPublisher', True)]
}%
% for instantiation, throwing in instantiations:
extension Publisher ${'' if throwing else 'where Failure == Never '}{
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public var values: ${instantiation}<Self> {
return .init(self)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
public struct ${instantiation}<Upstream: Publisher>: AsyncSequence
% if not throwing:
where Upstream.Failure == Never
% end
{
public typealias Element = Upstream.Output
public struct Iterator: AsyncIteratorProtocol {
public typealias Element = Upstream.Output
fileprivate let inner: Inner
public mutating func next() async ${'throws ' if throwing else ''}-> Element? {
return ${'try ' if throwing else ''}await withTaskCancellationHandler(
handler: { [inner] in inner.cancel() },
operation: { [inner] in ${'try ' if throwing else ''}await inner.next() }
)
}
}
public typealias AsyncIterator = Iterator
private let publisher: Upstream
public init(_ publisher: Upstream) {
self.publisher = publisher
}
public func makeAsyncIterator() -> Iterator {
let inner = Iterator.Inner()
publisher.subscribe(inner)
return Iterator(inner: inner)
}
}
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension ${instantiation}.Iterator {
fileprivate final class Inner: Subscriber, Cancellable {
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription
case subscribed(Subscription)
case terminal${'(Error?)' if throwing else ''}
}
private let lock = UnfairLock.allocate()
private var pending: [UnsafeContinuation<Input?, ${'Error' if throwing else 'Never'}>] = []
private var state = State.awaitingSubscription
private var pendingDemand = Subscribers.Demand.none
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = state else {
lock.unlock()
subscription.cancel()
return
}
state = .subscribed(subscription)
let pendingDemand = self.pendingDemand
self.pendingDemand = .none
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
return .none
}
precondition(!pending.isEmpty, "Received an output without requesting demand")
let continuation = pending.removeFirst()
lock.unlock()
continuation.resume(returning: input)
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
% if throwing:
switch state {
case .awaitingSubscription, .subscribed:
if let continuation = pending.first {
state = .terminal(nil)
let remaining = pending.take().dropFirst()
lock.unlock()
switch completion {
case .finished:
continuation.resume(returning: nil)
case .failure(let error):
continuation.resume(throwing: error)
}
remaining.resumeAllWithNil()
} else {
state = .terminal(completion.failure)
lock.unlock()
}
case .terminal:
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
}
% else:
state = .terminal
let pending = self.pending.take()
lock.unlock()
pending.resumeAllWithNil()
% end
}
func cancel() {
lock.lock()
let pending = self.pending.take()
guard case .subscribed(let subscription) = state else {
state = .terminal${'(nil)' if throwing else ''}
lock.unlock()
pending.resumeAllWithNil()
return
}
state = .terminal${'(nil)' if throwing else ''}
lock.unlock()
subscription.cancel()
pending.resumeAllWithNil()
}
fileprivate func next() async ${'throws ' if throwing else ''}-> Input? {
return ${'try ' if throwing else ''}await withUnsafe${'Throwing' if throwing else ''}Continuation { continuation in
lock.lock()
switch state {
case .awaitingSubscription:
pending.append(continuation)
pendingDemand += 1
lock.unlock()
case .subscribed(let subscription):
pending.append(continuation)
lock.unlock()
subscription.request(.max(1))
case .terminal${'(nil)' if throwing else ''}:
lock.unlock()
continuation.resume(returning: nil)
% if throwing:
case .terminal(let error?):
state = .terminal(nil)
lock.unlock()
continuation.resume(throwing: error)
% end
}
}
}
}
}
% end
@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *)
extension Sequence {
fileprivate func resumeAllWithNil<Output, Failure: Error>()
where Element == UnsafeContinuation<Output?, Failure>
{
for continuation in self {
continuation.resume(returning: nil)
}
}
}
#endif
+5 -10
View File
@@ -108,8 +108,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -181,13 +180,11 @@ extension CurrentValueSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
guard let downstream = self.downstream.take() else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -227,13 +224,11 @@ extension CurrentValueSubject {
override func cancel() {
lock.lock()
if self.downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
+66 -52
View File
@@ -43,8 +43,7 @@ public final class Future<Output, Failure: Error>: Publisher {
return
}
self.result = result
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
switch result {
case .success(let output):
@@ -87,12 +86,32 @@ extension Future {
CustomPlaygroundDisplayConvertible
where Downstream.Input == Output, Downstream.Failure == Failure
{
private enum State {
case active(Downstream, hasAnyDemand: Bool)
case terminal
fileprivate var parent: Future?
var downstream: Downstream? {
switch self {
case .active(let downstream, hasAnyDemand: _):
return downstream
case .terminal:
return nil
}
}
fileprivate var downstream: Downstream?
var hasAnyDemand: Bool {
switch self {
case .active(_, let hasAnyDemand):
return hasAnyDemand
case .terminal:
return false
}
}
}
fileprivate var hasAnyDemand = false
private var parent: Future?
private var state: State
private var lock = UnfairLock.allocate()
@@ -100,7 +119,7 @@ extension Future {
fileprivate init(parent: Future, downstream: Downstream) {
self.parent = parent
self.downstream = downstream
self.state = .active(downstream, hasAnyDemand: false)
}
deinit {
@@ -108,21 +127,8 @@ extension Future {
downstreamLock.deallocate()
}
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard let downstream = self.downstream else {
lock.unlock()
return
}
let parent = self.parent
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
fileprivate func lockedFulfill(downstream: Downstream,
result: Result<Output, Failure>) {
switch result {
case .success(let output):
_ = downstream.receive(output)
@@ -130,6 +136,24 @@ extension Future {
case .failure(let error):
downstream.receive(completion: .failure(error))
}
}
fileprivate func fulfill(_ result: Result<Output, Failure>) {
lock.lock()
guard case let .active(downstream, hasAnyDemand) = state else {
lock.unlock()
return
}
if case .success = result, !hasAnyDemand {
lock.unlock()
return
}
state = .terminal
lock.unlock()
downstreamLock.lock()
lockedFulfill(downstream: downstream, result: result)
let parent = self.parent.take()
downstreamLock.unlock()
parent?.disassociate(self)
}
@@ -150,47 +174,37 @@ extension Future {
override func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
lock.lock()
guard let downstream = self.downstream, let parent = self.parent else {
guard case .active(let downstream, hasAnyDemand: _) = state else {
lock.unlock()
return
}
hasAnyDemand = true
state = .active(downstream, hasAnyDemand: true)
parent.lock.lock()
guard let result = parent.result else {
parent.lock.unlock()
if let parent = parent, let result = parent.result {
// If the promise is already resolved, send the result downstream
// immediately
state = .terminal
lock.unlock()
downstreamLock.lock()
lockedFulfill(downstream: downstream, result: result)
downstreamLock.unlock()
parent.disassociate(self)
} else {
lock.unlock()
return
}
parent.lock.unlock()
self.downstream = nil
self.parent = nil
lock.unlock()
downstreamLock.lock()
switch result {
case .success(let output):
_ = downstream.receive(output)
downstream.receive(completion: .finished)
case .failure(let error):
// This branch is not reachable under normal circumstances,
// but may be reachable in case of a race condition.
downstream.receive(completion: .failure(error))
}
downstreamLock.unlock()
parent.disassociate(self)
}
override func cancel() {
lock.lock()
if self.downstream == nil {
switch state {
case .active:
state = .terminal
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
case .terminal:
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
lock.unlock()
parent?.disassociate(self)
}
var description: String { return "Future" }
@@ -200,8 +214,8 @@ extension Future {
defer { lock.unlock() }
let children: [Mirror.Child] = [
("parent", parent as Any),
("downstream", downstream as Any),
("hasAnyDemand", hasAnyDemand),
("downstream", state.downstream as Any),
("hasAnyDemand", state.hasAnyDemand),
("subject", parent as Any)
]
return Mirror(self, children: children)
@@ -11,6 +11,12 @@ internal enum ConduitList<Output, Failure: Error> {
case many(Set<ConduitBase<Output, Failure>>)
}
extension ConduitList: HasDefaultValue {
init() {
self = .empty
}
}
extension ConduitList {
internal mutating func insert(_ conduit: ConduitBase<Output, Failure>) {
switch self {
@@ -50,8 +56,4 @@ extension ConduitList {
self = .many(set)
}
}
internal mutating func removeAll() {
self = .empty
}
}
@@ -187,8 +187,7 @@ extension PublishedSubject {
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
@@ -48,8 +48,6 @@ internal class ReduceProducer<Downstream: Subscriber,
private var upstreamCompleted = false
private var empty = true
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
self.downstream = downstream
self.initial = initial
@@ -100,7 +98,9 @@ internal class ReduceProducer<Downstream: Subscriber,
return
}
upstreamCompleted = true
self.completed = downstreamRequested || empty
if downstreamRequested {
self.completed = true
}
let completed = self.completed
let result = self.result
lock.unlock()
@@ -157,7 +157,6 @@ extension ReduceProducer: Subscriber {
lock.unlock()
return .none
}
empty = false
lock.unlock()
// Combine doesn't hold the lock when calling `receive(newValue:)`.
+30
View File
@@ -0,0 +1,30 @@
//
// Utils.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
internal protocol HasDefaultValue {
init()
}
extension HasDefaultValue {
@inline(__always)
internal mutating func take() -> Self {
let taken = self
self = .init()
return taken
}
}
extension Array: HasDefaultValue {}
extension Dictionary: HasDefaultValue {}
extension Optional: HasDefaultValue {
init() {
self = nil
}
}
+5 -10
View File
@@ -85,8 +85,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
}
active = false
self.completion = completion
let downstreams = self.downstreams
self.downstreams.removeAll()
let downstreams = self.downstreams.take()
lock.unlock()
downstreams.forEach { conduit in
conduit.finish(completion: completion)
@@ -168,13 +167,11 @@ extension PassthroughSubject {
override func finish(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard let downstream = self.downstream else {
guard let downstream = self.downstream.take() else {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
downstreamLock.lock()
@@ -197,13 +194,11 @@ extension PassthroughSubject {
override func cancel() {
lock.lock()
if self.downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
self.downstream = nil
let parent = self.parent
self.parent = nil
let parent = self.parent.take()
lock.unlock()
parent?.disassociate(self)
}
@@ -222,8 +222,7 @@ extension Publishers.Encode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -252,11 +251,10 @@ extension Publishers.Encode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -336,8 +334,7 @@ extension Publishers.Decode {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -366,11 +363,10 @@ extension Publishers.Decode {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
+1 -2
View File
@@ -293,8 +293,7 @@ extension Just {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(value)
downstream.receive(completion: .finished)
}
@@ -122,8 +122,7 @@ extension Optional.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(output)
downstream.receive(completion: .finished)
}
@@ -331,9 +331,7 @@ extension Publishers.Buffer {
private func lockedPop(_ demand: Subscribers.Demand) -> [Input] {
assert(demand > 0)
guard let max = demand.max else {
let poppedValues = self.values
self.values = []
return poppedValues
return values.take()
}
let poppedValues = Array(values.prefix(max))
@@ -128,8 +128,7 @@ extension Publishers.CollectByCount {
lock.unlock()
return .none
}
let output = self.buffer
self.buffer = []
let output = self.buffer.take()
lock.unlock()
return downstream.receive(output) * count
}
@@ -143,8 +142,7 @@ extension Publishers.CollectByCount {
if buffer.isEmpty {
lock.unlock()
} else {
let buffer = self.buffer
self.buffer = []
let buffer = self.buffer.take()
lock.unlock()
_ = downstream.receive(buffer)
}
@@ -168,10 +166,9 @@ extension Publishers.CollectByCount {
func cancel() {
lock.lock()
if let subscription = self.subscription {
if let subscription = self.subscription.take() {
buffer = []
finished = true
self.subscription = nil
lock.unlock()
subscription.cancel()
} else {
@@ -233,7 +233,7 @@ extension Publishers.Concatenate {
private var suffixState = SubscriptionStatus.awaitingSubscription
private let suffix: Suffix
private var suffix: Suffix?
private var pending = Subscribers.Demand.none
@@ -266,8 +266,18 @@ extension Publishers.Concatenate {
prefixState.subscription ?? suffixState.subscription
prefixState = .terminal
suffixState = .terminal
lock.unlock()
upstreamSubscription?.cancel()
// We MUST release the object AFTER unlocking the lock,
// since releasing it may trigger execution of arbitrary code,
// for example, if the object has a deinit.
// When the object deallocates, its deinit is called, and holding
// the lock at that moment can lead to deadlocks.
withExtendedLifetime(suffix) {
suffix = nil
lock.unlock()
upstreamSubscription?.cancel()
}
}
var description: String { return "Concatenate" }
@@ -320,7 +330,7 @@ extension Publishers.Concatenate {
lock.unlock()
switch completion {
case .finished:
suffix.subscribe(SuffixSubscriber(inner: self))
suffix?.subscribe(SuffixSubscriber(inner: self))
case .failure:
downstream.receive(completion: completion)
}
@@ -212,8 +212,7 @@ extension Publishers.Debounce {
let generation = currentGeneration
currentValue = input
let due = scheduler.now.advanced(by: dueTime)
let previousCancellers = self.currentCancellers
currentCancellers.removeAll()
let previousCancellers = self.currentCancellers.take()
currentCancellers[generation] = .pending
lock.unlock()
let newCanceller = scheduler.schedule(after: due,
@@ -238,8 +237,7 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers
currentCancellers.removeAll()
let previousCancellers = currentCancellers.take()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -268,8 +266,7 @@ extension Publishers.Debounce {
return
}
state = .terminal
let previousCancellers = currentCancellers
currentCancellers.removeAll()
let previousCancellers = currentCancellers.take()
lock.unlock()
for canceller in previousCancellers.values {
canceller.cancel()
@@ -306,11 +303,10 @@ extension Publishers.Debounce {
return
}
guard let canceller = currentCancellers[generation] else {
guard let canceller = currentCancellers[generation].take() else {
lock.unlock()
return
}
currentCancellers[generation] = nil
let hasAnyDemand = downstreamDemand != .none
if hasAnyDemand {
@@ -139,8 +139,7 @@ extension Publishers.Drop {
func cancel() {
lock.lock()
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
}
@@ -204,8 +204,7 @@ extension Publishers.DropUntilOutput {
}
otherFinished = true
if let upstreamSubscription = self.upstreamSubscription {
self.upstreamSubscription = nil
if let upstreamSubscription = self.upstreamSubscription.take() {
lock.unlock()
upstreamSubscription.cancel()
} else {
@@ -229,10 +228,8 @@ extension Publishers.DropUntilOutput {
func cancel() {
lock.lock()
let upstreamSubscription = self.upstreamSubscription
let otherSubscription = self.otherSubscription
self.upstreamSubscription = nil
self.otherSubscription = nil
let upstreamSubscription = self.upstreamSubscription.take()
let otherSubscription = self.otherSubscription.take()
cancelled = true
lock.unlock()
@@ -221,8 +221,7 @@ extension Publishers.${instantiation} {
} catch {
lock.lock()
finished = true
let subscription = self.subscription
self.subscription = nil
let subscription = self.subscription.take()
lock.unlock()
subscription?.cancel()
downstream.receive(completion: .failure(error))
@@ -251,11 +250,10 @@ extension Publishers.${instantiation} {
func cancel() {
lock.lock()
guard !finished, let subscription = self.subscription else {
guard !finished, let subscription = self.subscription.take() else {
lock.unlock()
return
}
self.subscription = nil
finished = true
lock.unlock()
subscription.cancel()
@@ -14,7 +14,7 @@ extension Publisher {
/// the elements from one kind of publisher into a new publisher that is sent
/// to subscribers. Use `flatMap(maxPublishers:_:)` when you want to create a new
/// series of events for downstream subscribers based on the received value.
/// The closure creates the new `Publishe`` based on the received value.
/// The closure creates the new `Publisher` based on the received value.
/// The new `Publisher` can emit more than one event, and successful completion of
/// the new `Publisher` does not complete the overall stream.
/// Failure of the new `Publisher` will fail the overall stream.
@@ -304,8 +304,7 @@ extension Publishers.FlatMap {
}
if demand == .unlimited {
downstreamDemand = .unlimited
let buffer = self.buffer
self.buffer = []
let buffer = self.buffer.take()
let subscriptions = self.subscriptions
lock.unlock()
downstreamLock.lock()
@@ -361,10 +360,8 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let subscriptions = self.subscriptions.take()
let outerSubscription = self.outerSubscription.take()
lock.unlock()
for (_, subscription) in subscriptions {
subscription.cancel()
@@ -450,8 +447,7 @@ extension Publishers.FlatMap {
return
}
cancelledOrCompleted = true
let subscriptions = self.subscriptions
self.subscriptions = [:]
let subscriptions = self.subscriptions.take()
lock.unlock()
for (i, subscription) in subscriptions where i != index {
subscription.cancel()
@@ -0,0 +1,197 @@
//
// Publishers.PrefixUntilOutput.swift
//
//
// Created by Sergej Jaskiewicz on 08.11.2020.
//
extension Publisher {
/// Republishes elements until another publisher emits an element.
///
/// After the second publisher publishes an element, the publisher returned by this
/// method finishes.
///
/// - Parameter publisher: A second publisher.
/// - Returns: A publisher that republishes elements until the second publisher
/// publishes an element.
public func prefix<Other: Publisher>(
untilOutputFrom publisher: Other
) -> Publishers.PrefixUntilOutput<Self, Other> {
return .init(upstream: self, other: publisher)
}
}
extension Publishers {
public struct PrefixUntilOutput<Upstream: Publisher, Other: Publisher>: Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// Another publisher, whose first output causes this publisher to finish.
public let other: Other
public init(upstream: Upstream, other: Other) {
self.upstream = upstream
self.other = other
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
upstream.subscribe(Inner(downstream: subscriber, trigger: other))
}
}
}
extension Publishers.PrefixUntilOutput {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private struct Termination: Subscriber {
let inner: Inner
var combineIdentifier: CombineIdentifier {
return inner.combineIdentifier
}
func receive(subscription: Subscription) {
inner.terminationReceive(subscription: subscription)
}
func receive(_ input: Other.Output) -> Subscribers.Demand {
return inner.terminationReceive(input)
}
func receive(completion: Subscribers.Completion<Other.Failure>) {
inner.terminationReceive(completion: completion)
}
}
private var termination: Termination?
private var prefixState = SubscriptionStatus.awaitingSubscription
private var terminationState = SubscriptionStatus.awaitingSubscription
private var triggered = false
private let lock = UnfairLock.allocate()
private let downstream: Downstream
init(downstream: Downstream, trigger: Other) {
self.downstream = downstream
let termination = Termination(inner: self)
self.termination = termination
trigger.subscribe(termination)
}
deinit {
lock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = prefixState else {
lock.unlock()
subscription.cancel()
return
}
prefixState = triggered ? .terminal : .subscribed(subscription)
lock.unlock()
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = prefixState else {
lock.unlock()
return .none
}
lock.unlock()
return downstream.receive(input)
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
let prefixState = self.prefixState
let terminationSubscription = terminationState.subscription
self.prefixState = .terminal
terminationState = .terminal
termination = nil
lock.unlock()
terminationSubscription?.cancel()
if case .subscribed = prefixState {
downstream.receive(completion: completion)
}
}
func request(_ demand: Subscribers.Demand) {
lock.lock()
guard case let .subscribed(subscription) = prefixState else {
lock.unlock()
return
}
lock.unlock()
subscription.request(demand)
}
func cancel() {
lock.lock()
let prefixSubscription = prefixState.subscription
let terminationSubscription = terminationState.subscription
prefixState = .terminal
terminationState = .terminal
lock.unlock()
prefixSubscription?.cancel()
terminationSubscription?.cancel()
}
// MARK: - Private
private func terminationReceive(subscription: Subscription) {
lock.lock()
guard case .awaitingSubscription = terminationState else {
lock.unlock()
subscription.cancel()
return
}
terminationState = .subscribed(subscription)
lock.unlock()
subscription.request(.max(1))
}
private func terminationReceive(_ input: Other.Output) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = terminationState else {
lock.unlock()
return .none
}
let prefixSubscription = prefixState.subscription
prefixState = .terminal
terminationState = .terminal
termination = nil
triggered = true
lock.unlock()
prefixSubscription?.cancel()
downstream.receive(completion: .finished)
return .none
}
private func terminationReceive(
completion: Subscribers.Completion<Other.Failure>
) {
lock.lock()
terminationState = .terminal
termination = nil
lock.unlock()
}
}
}
@@ -75,9 +75,7 @@ extension Publishers {
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Upstream.Output == Downstream.Input, Downstream.Failure == Failure
{
let inner = Inner(downstream: subscriber, output: output)
upstream.subscribe(inner)
subscriber.receive(subscription: inner)
upstream.subscribe(Inner(downstream: subscriber, output: output))
}
}
}
@@ -123,11 +121,8 @@ extension Publishers.ReplaceError {
return
}
status = .subscribed(subscription)
let pendingDemand = self.pendingDemand
lock.unlock()
if pendingDemand != .none {
subscription.request(pendingDemand)
}
downstream.receive(subscription: self)
}
func receive(_ input: Input) -> Subscribers.Demand {
@@ -150,7 +145,7 @@ extension Publishers.ReplaceError {
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case .subscribed = status else {
guard case .subscribed = status, !terminated else {
lock.unlock()
return
}
@@ -230,8 +230,7 @@ extension Publishers.SwitchToLatest {
return .none
}
if let currentInnerSubscription = self.currentInnerSubscription {
self.currentInnerSubscription = nil
if let currentInnerSubscription = self.currentInnerSubscription.take() {
lock.unlock()
currentInnerSubscription.cancel()
lock.lock()
@@ -272,8 +271,7 @@ extension Publishers.SwitchToLatest {
lock.unlock()
}
case .failure:
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let currentInnerSubscription = self.currentInnerSubscription.take()
sentCompletion = true
lock.unlock()
currentInnerSubscription?.cancel()
@@ -298,10 +296,8 @@ extension Publishers.SwitchToLatest {
func cancel() {
lock.lock()
cancelled = true
let currentInnerSubscription = self.currentInnerSubscription
self.currentInnerSubscription = nil
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let currentInnerSubscription = self.currentInnerSubscription.take()
let outerSubscription = self.outerSubscription.take()
lock.unlock()
currentInnerSubscription?.cancel()
@@ -386,8 +382,7 @@ extension Publishers.SwitchToLatest {
return
}
cancelled = true
let outerSubscription = self.outerSubscription
self.outerSubscription = nil
let outerSubscription = self.outerSubscription.take()
sentCompletion = true
lock.unlock()
outerSubscription?.cancel()
@@ -0,0 +1,359 @@
//
// Publishers.Throttle.swift
//
//
// Created by Stuart Austin on 14/11/2020.
//
extension Publisher {
// swiftlint:disable generic_type_name line_length
/// Publishes either the most-recent or first element published by the upstream
/// publisher in the specified time interval.
///
/// Use `throttle(for:scheduler:latest:`` to selectively republish elements from
/// an upstream publisher during an interval you specify. Other elements received from
/// the upstream in the throttling interval arent republished.
///
/// In the example below, a `Timer.TimerPublisher` produces elements on 3-second
/// intervals; the `throttle(for:scheduler:latest:)` operator delivers the first
/// event, then republishes only the latest event in the following ten second
/// intervals:
///
/// cancellable = Timer.publish(every: 3.0, on: .main, in: .default)
/// .autoconnect()
/// .print("\(Date().description)")
/// .throttle(for: 10.0, scheduler: RunLoop.main, latest: true)
/// .sink(
/// receiveCompletion: { print ("Completion: \($0).") },
/// receiveValue: { print("Received Timestamp \($0).") }
/// )
///
/// // Prints:
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:26:57 +0000)
/// // Received Timestamp 2020-03-19 18:26:57 +0000.
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:00 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:03 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:06 +0000)
/// // Publish at: 2020-03-19 18:26:54 +0000: receive value: (2020-03-19 18:27:09 +0000)
/// // Received Timestamp 2020-03-19 18:27:09 +0000.
///
/// - Parameters:
/// - interval: The interval at which to find and emit either the most recent or
/// the first element, expressed in the time system of the scheduler.
/// - scheduler: The scheduler on which to publish elements.
/// - latest: A Boolean value that indicates whether to publish the most recent
/// element. If `false`, the publisher emits the first element received during
/// the interval.
/// - Returns: A publisher that emits either the most-recent or first element received
/// during the specified interval.
public func throttle<S>(for interval: S.SchedulerTimeType.Stride,
scheduler: S,
latest: Bool) -> Publishers.Throttle<Self, S>
where S: Scheduler
{
return .init(upstream: self,
interval: interval,
scheduler: scheduler,
latest: latest)
}
// swiftlint:enable generic_type_name line_length
}
extension Publishers {
/// A publisher that publishes either the most-recent or first element published by
/// the upstream publisher in a specified time interval.
public struct Throttle<Upstream, Context>: Publisher
where Upstream: Publisher, Context: Scheduler
{
/// The kind of values published by this publisher.
public typealias Output = Upstream.Output
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The interval in which to find and emit the most recent element.
public let interval: Context.SchedulerTimeType.Stride
/// The scheduler on which to publish elements.
public let scheduler: Context
/// A Boolean value indicating whether to publish the most recent element.
///
/// If `false`, the publisher emits the first element received during
/// the interval.
public let latest: Bool
public init(upstream: Upstream,
interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool) {
self.upstream = upstream
self.interval = interval
self.scheduler = scheduler
self.latest = latest
}
// swiftlint:disable generic_type_name
/// Attaches the specified subscriber to this publisher.
///
/// Implementations of ``Publisher`` must implement this method.
///
/// The provided implementation of ``Publisher/subscribe(_:)-4u8kn``calls
/// this method.
///
/// - Parameter subscriber: The subscriber to attach to this ``Publisher``,
/// after which it can receive values.
public func receive<S>(subscriber: S)
where S: Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
{
let inner = Inner(interval: interval,
scheduler: scheduler,
latest: latest,
downstream: subscriber)
upstream.subscribe(inner)
}
// swiftlint:enable generic_type_name
}
}
extension Publishers.Throttle {
private final class Inner<Downstream: Subscriber>
: Subscriber,
Subscription,
CustomStringConvertible,
CustomReflectable,
CustomPlaygroundDisplayConvertible
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
private enum State {
case awaitingSubscription(Downstream)
case subscribed(Subscription, Downstream)
case pendingTerminal(Subscription, Downstream)
case terminal
}
private let lock = UnfairLock.allocate()
private let interval: Context.SchedulerTimeType.Stride
private let scheduler: Context
private let latest: Bool
private var state: State
private let downstreamLock = UnfairRecursiveLock.allocate()
private var lastEmissionTime: Context.SchedulerTimeType?
private var pendingInput: Input?
private var pendingCompletion: Subscribers.Completion<Failure>?
private var demand: Subscribers.Demand = .none
private var lastTime: Context.SchedulerTimeType
init(interval: Context.SchedulerTimeType.Stride,
scheduler: Context,
latest: Bool,
downstream: Downstream) {
self.state = .awaitingSubscription(downstream)
self.interval = interval
self.scheduler = scheduler
self.latest = latest
self.lastTime = scheduler.now
}
deinit {
lock.deallocate()
downstreamLock.deallocate()
}
func receive(subscription: Subscription) {
lock.lock()
guard case let .awaitingSubscription(downstream) = state else {
lock.unlock()
subscription.cancel()
return
}
self.lastTime = scheduler.now
state = .subscribed(subscription, downstream)
lock.unlock()
subscription.request(.unlimited)
downstreamLock.lock()
downstream.receive(subscription: self)
downstreamLock.unlock()
}
func receive(_ input: Input) -> Subscribers.Demand {
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return .none
}
let lastTime = scheduler.now
self.lastTime = lastTime
guard demand > .none else {
lock.unlock()
return .none
}
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && latest {
pendingInput = input
lock.unlock()
} else if !hasScheduledOutput {
let minimumEmissionTime =
lastEmissionTime.map { $0.advanced(by: interval) }
let emissionTime =
minimumEmissionTime.map { Swift.max(lastTime, $0) } ?? lastTime
demand -= 1
pendingInput = input
lock.unlock()
let action: () -> Void = { [weak self] in
self?.scheduledEmission()
}
if emissionTime == lastTime {
scheduler.schedule(action)
} else {
scheduler.schedule(after: emissionTime, action)
}
} else {
lock.unlock()
}
return .none
}
func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
guard case let .subscribed(subscription, downstream) = state else {
lock.unlock()
return
}
let lastTime = scheduler.now
self.lastTime = lastTime
state = .pendingTerminal(subscription, downstream)
let hasScheduledOutput = (pendingInput != nil || pendingCompletion != nil)
if hasScheduledOutput && pendingCompletion == nil {
pendingCompletion = completion
lock.unlock()
} else if !hasScheduledOutput {
pendingCompletion = completion
lock.unlock()
scheduler.schedule { [weak self] in
self?.scheduledEmission()
}
} else {
lock.unlock()
}
}
private func scheduledEmission() {
lock.lock()
let downstream: Downstream
switch state {
case .awaitingSubscription, .terminal:
lock.unlock()
return
case let .subscribed(_, foundDownstream),
let .pendingTerminal(_, foundDownstream):
downstream = foundDownstream
}
if self.pendingInput != nil && self.pendingCompletion == nil {
lastEmissionTime = scheduler.now
}
let pendingInput = self.pendingInput.take()
let pendingCompletion = self.pendingCompletion.take()
if pendingCompletion != nil {
state = .terminal
}
lock.unlock()
downstreamLock.lock()
let newDemand: Subscribers.Demand
if let input = pendingInput {
newDemand = downstream.receive(input)
} else {
newDemand = .none
}
if let completion = pendingCompletion {
downstream.receive(completion: completion)
}
downstreamLock.unlock()
guard newDemand > 0 else { return }
self.lock.lock()
demand += newDemand
self.lock.unlock()
}
func request(_ demand: Subscribers.Demand) {
guard demand > 0 else { return }
lock.lock()
guard case .subscribed = state else {
lock.unlock()
return
}
self.demand += demand
lock.unlock()
}
func cancel() {
lock.lock()
let subscription: Subscription?
switch state {
case let .subscribed(existingSubscription, _),
let .pendingTerminal(existingSubscription, _):
subscription = existingSubscription
case .awaitingSubscription, .terminal:
subscription = nil
}
state = .terminal
lock.unlock()
subscription?.cancel()
}
var description: String { return "Throttle" }
var customMirror: Mirror { return Mirror(self, children: EmptyCollection()) }
var playgroundDescription: Any { return description }
}
}
@@ -0,0 +1,720 @@
//
// Publishers.Zip.swift
//
// Created by Eric Patey on 29.08.2019.
//
#if canImport(COpenCombineHelpers)
import COpenCombineHelpers
#endif
extension Publishers {
/// A publisher created by applying the zip function to two upstream publishers.
public struct Zip<UpstreamA: Publisher, UpstreamB: Publisher>: Publisher
where UpstreamA.Failure == UpstreamB.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (UpstreamA.Output, UpstreamB.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public init(_ a: UpstreamA, _ b: UpstreamB) {
self.a = a
self.b = b
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream) where
UpstreamB.Failure == Downstream.Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b)
}
}
/// A publisher created by applying the zip function to three upstream publishers.
public struct Zip3<UpstreamA: Publisher,
UpstreamB: Publisher,
UpstreamC: Publisher>
: Publisher
where UpstreamA.Failure == UpstreamB.Failure,
UpstreamB.Failure == UpstreamC.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public let c: UpstreamC
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
self.a = a
self.b = b
self.c = c
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream>(subscriber: Downstream)
where Downstream: Subscriber,
UpstreamC.Failure == Downstream.Failure,
// swiftlint:disable:next large_tuple
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b, c)
}
}
/// A publisher created by applying the zip function to four upstream publishers.
public struct Zip4<
UpstreamA: Publisher,
UpstreamB: Publisher,
UpstreamC: Publisher,
UpstreamD: Publisher
>: Publisher where
UpstreamA.Failure == UpstreamB.Failure,
UpstreamB.Failure == UpstreamC.Failure,
UpstreamC.Failure == UpstreamD.Failure
{
/// The kind of values published by this publisher.
public typealias Output = (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
/// The kind of errors this publisher might publish.
///
/// Use `Never` if this `Publisher` does not publish errors.
public typealias Failure = UpstreamA.Failure
public let a: UpstreamA
public let b: UpstreamB
public let c: UpstreamC
public let d: UpstreamD
public init(_ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC, _ d: UpstreamD) {
self.a = a
self.b = b
self.c = c
self.d = d
}
/// This function is called to attach the specified `Subscriber` to this
/// `Publisher` by `subscribe(_:)`
///
/// - SeeAlso: `subscribe(_:)`
/// - Parameters:
/// - subscriber: The subscriber to attach to this `Publisher`.
/// once attached it can begin to receive values.
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where UpstreamD.Failure == Downstream.Failure,
// swiftlint:disable:next large_tuple
Downstream.Input == (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
{
_ = Inner<Downstream>(downstream: subscriber, a, b, c, d)
}
}
}
extension Publisher {
/// Combine elements from another publisher and deliver pairs of elements as tuples.
///
/// The returned publisher waits until both publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher together as a tuple to
/// the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a
/// tuple with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the
/// zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers
/// as tuples.
public func zip<Other>(_ other: Other) -> Publishers.Zip<Self, Other>
where Other: Publisher, Self.Failure == Other.Failure
{
return Publishers.Zip(self, other)
}
/// Combine elements from another publisher and deliver a transformed output.
///
/// The returned publisher waits until both publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher together as a tuple to
/// the subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits event `c`, the zip publisher emits the tuple `(a, c)`. It wont emit a tuple
/// with event `b` until `P2` emits another event.
/// If either upstream publisher finishes successfuly or fails with an error, the
/// zipped publisher does the same.
///
/// - Parameter other: Another publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits pairs of elements from the upstream publishers
/// as tuples.
public func zip<Other, Result>(
_ other: Other,
_ transform: @escaping (Self.Output, Other.Output) -> Result)
-> Publishers.Map<Publishers.Zip<Self, Other>, Result>
where Other: Publisher, Self.Failure == Other.Failure
{
return Publishers.Map(upstream: Publishers.Zip(self, other), transform: transform)
}
/// Combine elements from two other publishers and deliver groups of elements as
/// tuples.
///
/// The returned publisher waits until all three publishers have emitted an event,
/// then delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip
/// publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or
/// `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2>(_ publisher1: Other1, _ publisher2: Other2)
-> Publishers.Zip3<Self, Other1, Other2>
where Other1: Publisher,
Other2: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure
{
return Publishers.Zip3(self, publisher1, publisher2)
}
/// Combine elements from two other publishers and deliver a transformed output.
///
/// The returned publisher waits until all three publishers have emitted an event,
/// then delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the event `e`, the zip
/// publisher emits the tuple `(a, c, e)`. It wont emit a tuple with elements `b` or
/// `d` until `P3` emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Result>(
_ publisher1: Other1,
_ publisher2: Other2,
_ transform: @escaping (Self.Output, Other1.Output, Other2.Output) -> Result)
-> Publishers.Map<Publishers.Zip3<Self, Other1, Other2>, Result>
where Other1: Publisher,
Other2: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure
{
return Publishers.Map(upstream: Publishers.Zip3(self, publisher1, publisher2),
transform: transform)
}
/// Combine elements from three other publishers and deliver groups of elements as
/// tuples.
///
/// The returned publisher waits until all four publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and
/// publisher `P4` emits the event `g`, the zip publisher emits the tuple
/// `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4`
/// emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Other3>(_ publisher1: Other1,
_ publisher2: Other2,
_ publisher3: Other3)
-> Publishers.Zip4<Self, Other1, Other2, Other3>
where Other1: Publisher,
Other2: Publisher,
Other3: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure,
Other2.Failure == Other3.Failure
{
return Publishers.Zip4(self, publisher1, publisher2, publisher3)
}
/// Combine elements from three other publishers and deliver a transformed output.
///
/// The returned publisher waits until all four publishers have emitted an event, then
/// delivers the oldest unconsumed event from each publisher as a tuple to the
/// subscriber.
/// For example, if publisher `P1` emits elements `a` and `b`, and publisher `P2`
/// emits elements `c` and `d`, and publisher `P3` emits the elements `e` and `f`, and
/// publisher `P4` emits the event `g`, the zip publisher emits the tuple
/// `(a, c, e, g)`. It wont emit a tuple with elements `b`, `d`, or `f` until `P4`
/// emits another event.
/// If any upstream publisher finishes successfuly or fails with an error, the zipped
/// publisher does the same.
///
/// - Parameters:
/// - publisher1: A second publisher.
/// - publisher2: A third publisher.
/// - publisher3: A fourth publisher.
/// - transform: A closure that receives the most recent value from each publisher
/// and returns a new value to publish.
/// - Returns: A publisher that emits groups of elements from the upstream publishers
/// as tuples.
public func zip<Other1, Other2, Other3, Result>(
_ publisher1: Other1,
_ publisher2: Other2,
_ publisher3: Other3,
_ transform: @escaping (Self.Output, Other1.Output, Other2.Output, Other3.Output)
-> Result)
-> Publishers.Map<Publishers.Zip4<Self, Other1, Other2, Other3>, Result>
where Other1: Publisher,
Other2: Publisher,
Other3: Publisher,
Self.Failure == Other1.Failure,
Other1.Failure == Other2.Failure,
Other2.Failure == Other3.Failure
{
return Publishers.Map(upstream: Publishers.Zip4(self,
publisher1,
publisher2,
publisher3),
transform: transform)
}
}
extension Publishers.Zip {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB) {
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(), bSubscriber.dequeueValue())
}
}
}
extension Publishers.Zip3 {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (UpstreamA.Output, UpstreamB.Output, UpstreamC.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
init(downstream: Downstream, _ a: UpstreamA, _ b: UpstreamB, _ c: UpstreamC) {
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
c.subscribe(cSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber, cSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(),
bSubscriber.dequeueValue(),
cSubscriber.dequeueValue())
}
}
}
extension Publishers.Zip4 {
private class Inner<Downstream: Subscriber>: InnerBase<Downstream>
where Downstream.Failure == Failure,
Downstream.Input == (
UpstreamA.Output,
UpstreamB.Output,
UpstreamC.Output,
UpstreamD.Output)
{
private lazy var aSubscriber = ChildSubscriber<UpstreamA, Downstream>(self, 0)
private lazy var bSubscriber = ChildSubscriber<UpstreamB, Downstream>(self, 1)
private lazy var cSubscriber = ChildSubscriber<UpstreamC, Downstream>(self, 2)
private lazy var dSubscriber = ChildSubscriber<UpstreamD, Downstream>(self, 3)
init(downstream: Downstream,
_ a: UpstreamA,
_ b: UpstreamB,
_ c: UpstreamC,
_ d: UpstreamD)
{
super.init(downstream: downstream)
a.subscribe(aSubscriber)
b.subscribe(bSubscriber)
c.subscribe(cSubscriber)
d.subscribe(dSubscriber)
}
override fileprivate var upstreamSubscriptions: [ChildSubscription] {
return [aSubscriber, bSubscriber, cSubscriber, dSubscriber]
}
override fileprivate func dequeueValue() -> Downstream.Input {
return (aSubscriber.dequeueValue(),
bSubscriber.dequeueValue(),
cSubscriber.dequeueValue(),
dSubscriber.dequeueValue())
}
}
}
private class InnerBase<Downstream: Subscriber>: CustomStringConvertible {
let description = "Zip"
private let lock = UnfairRecursiveLock.allocate()
private let downstream: Downstream
private var downstreamDemand = Subscribers.Demand.none
private var valueIsBeingProcessed = false
private var value: Downstream.Input?
private var isFinished = false
// The following two pieces of state are a hacky implementation of subtle Apple
// concurrency behaviors. Specifically, when Zip is processing an upstream child value
// and sending a resulting value downstream, multiple behaviors are changed.
// 1. If a downstream demand request comes in during this period, the demand request
// for that specific triggering upstream child will be communiated via the result
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
// (AppleRef: 001)
// 2. If an upstream `.finished` comes in during this time period, the "finished
// asssessment check" (AppleRef: 002) is skipped.
// If an upstream value is being processed when a downstream demand request comes in,
// the demand for that specfic upstream child will be communiated via the result
// of `.receive(_ input:)` INSTEAD of a later `.request(_ demand:)` call.
private final var processingValueForChild: ChildSubscription?
private final var demandReceivedWhileProcessing: Subscribers.Demand?
init(downstream: Downstream) {
self.downstream = downstream
}
deinit {
lock.deallocate()
}
fileprivate var upstreamSubscriptions: [ChildSubscription] {
abstractMethod()
}
fileprivate func dequeueValue() -> Downstream.Input {
abstractMethod()
}
fileprivate final func receivedSubscription(for child: ChildSubscription) {
lock.lock()
child.state = .active
let sendSubscriptionDownstream = upstreamSubscriptions
.filter { $0.state == .waitingForSubscription }
.isEmpty
lock.unlock()
if sendSubscriptionDownstream {
self.sendSubscriptionDownstream()
}
}
fileprivate final func receivedChildValue(
child: ChildSubscription,
_ lockedStoreValue: () -> Void
) -> Subscribers.Demand {
lock.lock()
lockedStoreValue()
defer {
checkShouldFinish()
lock.unlock()
}
if let dequeuedValue = maybeDequeueValue() {
value = dequeuedValue
assert(processingValueForChild == nil)
processingValueForChild = child
valueIsBeingProcessed = true
return processValue() ?? .none
} else {
return .none
}
}
fileprivate final func receivedCompletion(
_ completion: Subscribers.Completion<Downstream.Failure>,
forChild child: ChildSubscription)
{
switch completion {
case .failure:
downstream.receive(completion: completion)
lock.lock()
child.state = .failed
let subscriptionsToCancel = upstreamSubscriptions
lock.unlock()
subscriptionsToCancel.forEach { $0.cancel() }
case .finished:
lock.lock()
child.state = .finished
if !valueIsBeingProcessed {
valueIsBeingProcessed = true
if processingValueForChild == nil &&
!areMoreValuesPossible &&
!isFinished {
sendFinishDownstream()
} else {
processValue()
}
}
lock.unlock()
}
}
private func checkShouldFinish() {
if processingValueForChild == nil && upstreamSubscriptions.shouldFinish() {
sendFinishDownstream()
isFinished = true
}
}
private func maybeDequeueValue() -> Downstream.Input? {
return hasCompleteValueAvailable ? dequeueValue() : nil
}
private func sendSubscriptionDownstream() {
downstream.receive(subscription: self)
}
private var hasCompleteValueAvailable: Bool {
return upstreamSubscriptions.allSatisfy { $0.hasValue }
}
private var areMoreValuesPossible: Bool {
// More values are possible if all children are (active || have surplus)
return upstreamSubscriptions
.allSatisfy { $0.state == .active || $0.hasValue }
}
@discardableResult
private func processValue() -> Subscribers.Demand? {
assert(valueIsBeingProcessed)
lock.lock()
defer {
valueIsBeingProcessed = false
processingValueForChild = nil
demandReceivedWhileProcessing = nil
lock.unlock()
}
if let value = self.value {
if downstreamDemand != .none {
downstreamDemand -= 1
}
let newDemand = downstream.receive(value)
if newDemand != .none {
downstreamDemand += newDemand
demandReceivedWhileProcessing = newDemand
}
self.value = nil
}
return demandReceivedWhileProcessing
}
private func sendRequestUpstream(demand: Subscribers.Demand) {
lock.lock()
let subscriptionsToRequest = upstreamSubscriptions
.filter { $0.childIndex != processingValueForChild?.childIndex }
lock.unlock()
subscriptionsToRequest.forEach { $0.request(demand) }
}
private func sendFinishDownstream() {
downstream.receive(completion: .finished)
lock.lock()
let activeChildren = upstreamSubscriptions.filter { $0.state == .active }
lock.unlock()
activeChildren.forEach { $0.cancel() }
}
}
extension InnerBase: Subscription {
fileprivate final func request(_ demand: Subscribers.Demand) {
guard demand != .none else {
fatalError()
}
lock.lock()
downstreamDemand += demand
sendRequestUpstream(demand: demand)
if valueIsBeingProcessed {
demandReceivedWhileProcessing = demand
} else {
valueIsBeingProcessed = true
processValue()
}
lock.unlock()
}
fileprivate final func cancel() {
lock.lock()
let subscriptionsToCancel = upstreamSubscriptions
lock.unlock()
subscriptionsToCancel.forEach { $0.cancel() }
}
}
extension Array where Element == ChildSubscription {
func shouldFinish() -> Bool {
for subscription in self
where subscription.state == .finished && !subscription.hasValue{
return true
}
return false
}
}
private enum ChildState {
case waitingForSubscription
case active
case finished
case failed
case canceled
}
// Note that it's critical that this protocol not have any associated types - specifically
// note that it does not refer to `Upstream`.
// This allows `InnerBase` to do most of the heavy lifting without regard to the
// upstream publisher's value type.
private protocol ChildSubscription: AnyObject, Subscription {
var state: ChildState { get set }
var childIndex: Int { get }
var hasValue: Bool { get }
}
fileprivate final class ChildSubscriber<Upstream: Publisher, Downstream: Subscriber>
where Upstream.Failure == Downstream.Failure
{
typealias Input = Upstream.Output
typealias Failure = Upstream.Failure
fileprivate final var state: ChildState = .waitingForSubscription
fileprivate final var upstreamSubscription: Subscription?
private var values = [Upstream.Output]()
private unowned let parent: InnerBase<Downstream>
fileprivate let childIndex: Int
init(_ parent: InnerBase<Downstream>, _ childIndex: Int) {
self.parent = parent
self.childIndex = childIndex
}
fileprivate final func dequeueValue() -> Upstream.Output {
return values.remove(at: 0)
}
}
extension ChildSubscriber: ChildSubscription {
fileprivate final var hasValue: Bool {
return !values.isEmpty
}
}
extension ChildSubscriber: Subscription {
fileprivate final func request(_ demand: Subscribers.Demand) {
upstreamSubscription?.request(demand)
}
}
extension ChildSubscriber: Cancellable {
fileprivate final func cancel() {
upstreamSubscription?.cancel()
upstreamSubscription = nil
}
}
extension ChildSubscriber: Subscriber {
fileprivate final func receive(subscription: Subscription) {
if upstreamSubscription == nil {
upstreamSubscription = subscription
parent.receivedSubscription(for: self)
} else {
subscription.cancel()
}
}
fileprivate final func receive(_ input: Input) -> Subscribers.Demand {
return parent.receivedChildValue(child: self) { values.append(input) }
}
fileprivate final func receive(completion: Subscribers.Completion<Failure>) {
parent.receivedCompletion(completion, forChild: self)
}
}
@@ -136,8 +136,7 @@ extension Result.OCombine {
func request(_ demand: Subscribers.Demand) {
demand.assertNonZero()
guard let downstream = self.downstream else { return }
self.downstream = nil
guard let downstream = self.downstream.take() else { return }
_ = downstream.receive(output)
downstream.receive(completion: .finished)
}
@@ -112,8 +112,17 @@ extension Subscribers {
lock.assertOwner()
#endif
status = .terminal
object = nil
lock.unlock()
// We MUST release the object AFTER unlocking the lock,
// since releasing it may trigger execution of arbitrary code,
// for example, if the object has a deinit.
// When the object deallocates, its deinit is called, and holding
// the lock at that moment can lead to deadlocks.
withExtendedLifetime(object) {
object = nil
lock.unlock()
}
}
}
}
@@ -5,6 +5,10 @@
// Created by Sergej Jaskiewicz on 11.06.2019.
//
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
extension Subscribers {
/// A signal that a publisher doesnt produce additional elements, either due to
@@ -23,6 +27,10 @@ extension Subscribers.Completion: Equatable where Failure: Equatable {}
extension Subscribers.Completion: Hashable where Failure: Hashable {}
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Subscribers.Completion: Sendable {}
#endif
extension Subscribers.Completion {
private enum CodingKeys: String, CodingKey {
case success = "success"
@@ -70,4 +78,13 @@ extension Subscribers.Completion {
return .failure(error)
}
}
internal var failure: Failure? {
switch self {
case .finished:
return nil
case .failure(let failure):
return failure
}
}
}
@@ -7,6 +7,10 @@
// swiftlint:disable shorthand_operator - because of false positives here
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
extension Subscribers {
/// A requested number of items, sent to a publisher from a subscriber through
@@ -466,3 +470,7 @@ extension Subscribers {
}
}
}
#if canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)
extension Subscribers.Demand: Sendable {}
#endif
@@ -73,8 +73,22 @@ extension Subscribers {
public func receive(completion: Subscribers.Completion<Failure>) {
lock.lock()
status = .terminal
let receiveCompletion = self.receiveCompletion
terminateAndConsumeLock()
self.receiveCompletion = { _ in }
// We MUST release the closures AFTER unlocking the lock,
// since releasing a closure may trigger execution of arbitrary code,
// for example, if the closure captures an object with a deinit.
// When closure deallocates, the object's deinit is called, and holding
// the lock at that moment can lead to deadlocks.
// See https://github.com/OpenCombine/OpenCombine/issues/208
withExtendedLifetime(receiveValue) {
receiveValue = { _ in }
lock.unlock()
}
receiveCompletion(completion)
}
@@ -84,18 +98,21 @@ extension Subscribers {
lock.unlock()
return
}
terminateAndConsumeLock()
subscription.cancel()
}
private func terminateAndConsumeLock() {
#if DEBUG
lock.assertOwner()
#endif
status = .terminal
receiveValue = { _ in }
receiveCompletion = { _ in }
lock.unlock()
// We MUST release the closures AFTER unlocking the lock,
// since releasing a closure may trigger execution of arbitrary code,
// for example, if the closure captures an object with a deinit.
// When closure deallocates, the object's deinit is called, and holding
// the lock at that moment can lead to deadlocks.
// See https://github.com/OpenCombine/OpenCombine/issues/208
withExtendedLifetime((receiveValue, receiveCompletion)) {
receiveCompletion = { _ in }
receiveValue = { _ in }
lock.unlock()
}
subscription.cancel()
}
}
}
@@ -104,8 +104,17 @@ extension DispatchQueue {
/// value of the conforming type.
public typealias Magnitude = Int
private var _nanoseconds: Int64
/// The value of this time interval in nanoseconds.
public var magnitude: Int
public var magnitude: Int {
get {
return Int(_nanoseconds)
}
set {
_nanoseconds = Int64(newValue)
}
}
/// A `DispatchTimeInterval` created with the value of this type
/// in nanoseconds.
@@ -113,8 +122,8 @@ extension DispatchQueue {
return .nanoseconds(magnitude)
}
private init(magnitude: Int) {
self.magnitude = magnitude
private init(magnitude: Int64) {
_nanoseconds = magnitude
}
/// Creates a dispatch queue time interval from the given
@@ -204,7 +213,7 @@ extension DispatchQueue {
}
public static func < (lhs: Stride, rhs: Stride) -> Bool {
return lhs.magnitude < rhs.magnitude
return lhs._nanoseconds < rhs._nanoseconds
}
public static func * (lhs: Stride, rhs: Stride) -> Stride {
@@ -212,43 +221,51 @@ extension DispatchQueue {
}
public static func + (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs.magnitude + rhs.magnitude)
return Stride(magnitude: lhs._nanoseconds + rhs._nanoseconds)
}
public static func - (lhs: Stride, rhs: Stride) -> Stride {
return Stride(magnitude: lhs.magnitude - rhs.magnitude)
return Stride(magnitude: lhs._nanoseconds - rhs._nanoseconds)
}
public static func -= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude -= rhs.magnitude
lhs._nanoseconds -= rhs._nanoseconds
}
public static func *= (lhs: inout Stride, rhs: Stride) {
lhs.magnitude = 0
lhs._nanoseconds = 0
}
public static func += (lhs: inout Stride, rhs: Stride) {
lhs.magnitude += rhs.magnitude
lhs._nanoseconds += rhs._nanoseconds
}
public static func seconds(_ value: Double) -> Stride {
return Stride(magnitude: Int(value * 1_000_000_000))
let nanoseconds = value * 1_000_000_000
if nanoseconds >= Double(Int64.max) {
return Stride(magnitude: .max)
}
if nanoseconds <= Double(Int64.min) {
return Stride(magnitude: .min)
}
return Stride(magnitude: Int64(nanoseconds))
}
public static func seconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000_000_000))
return Stride(magnitude: clampedIntProduct(Int64(value),
1_000_000_000))
}
public static func milliseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000_000))
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000_000))
}
public static func microseconds(_ value: Int) -> Stride {
return Stride(magnitude: clampedIntProduct(value, 1_000))
return Stride(magnitude: clampedIntProduct(Int64(value), 1_000))
}
public static func nanoseconds(_ value: Int) -> Stride {
return Stride(magnitude: value)
return Stride(magnitude: Int64(value))
}
}
}
@@ -387,10 +404,10 @@ extension DispatchQueue: OpenCombine.Scheduler {
// This function is taken from swift-corelibs-libdispatch:
// https://github.com/apple/swift-corelibs-libdispatch/blob/c992dacf3ca114806e6ac9ffc9113b19255be9fe/src/swift/Time.swift#L134-L144
//
// Returns m1 * m2, clamped to the range [Int.min, Int.max].
// Returns m1 * m2, clamped to the range [Int64.min, Int64.max].
// Because of the way this function is used, we can always assume
// that m2 > 0.
private func clampedIntProduct(_ lhs: Int, _ rhs: Int) -> Int {
private func clampedIntProduct(_ lhs: Int64, _ rhs: Int64) -> Int64 {
assert(rhs > 0, "multiplier must be positive")
let (result, overflow) = lhs.multipliedReportingOverflow(by: rhs)
if overflow {
@@ -5,7 +5,10 @@
// Created by Sergej Jaskiewicz on 28.10.2020.
//
#if canImport(CoreFoundation)
import CoreFoundation
#endif
import Foundation
/// Use CoreFoundation on Darwin, since some pure
@@ -93,6 +96,7 @@ internal struct Timer {
#endif
}
#if canImport(CoreFoundation)
fileprivate func getCFRunLoopTimer() -> CFRunLoopTimer? {
#if canImport(Darwin)
return underlyingTimer
@@ -113,6 +117,7 @@ internal struct Timer {
fatalError("unreachable")
#endif
}
#endif // canImport(CoreFoundation)
}
extension RunLoop {
@@ -138,6 +143,7 @@ extension RunLoop {
}
}
#if canImport(CoreFoundation)
extension RunLoop.Mode {
fileprivate func asCFRunLoopMode() -> CFRunLoopMode {
#if canImport(Darwin)
@@ -159,3 +165,4 @@ extension RunLoop.Mode {
#endif
}
}
#endif // canImport(CoreFoundation)
@@ -0,0 +1,14 @@
//
// Utils.swift
//
//
// Created by Sergej Jaskiewicz on 28.08.2021.
//
extension Optional {
internal mutating func take() -> Optional {
let taken = self
self = nil
return taken
}
}
@@ -200,13 +200,13 @@ extension Notification {
func cancel() {
lock.lock()
guard let center = self.center, let observation = self.observation else {
guard let center = self.center.take(),
let observation = self.observation.take()
else {
lock.unlock()
return
}
self.center = nil
self.object = nil
self.observation = nil
lock.unlock()
center.removeObserver(observation)
}
@@ -234,19 +234,16 @@ extension OperationQueue {
}
override func main() {
guard let action = self.action else { return }
self.action = nil
guard let action = self.action.take() else { return }
action()
guard let queue = self.queue,
let context = self.context
guard let queue = self.queue.take(),
let context = self.context.take()
else {
self.queue = nil
self.context = nil
return
}
self.queue = nil
self.context = nil
context.lock.lock()
if context.operation == nil {
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 13.12.2019.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -5,7 +5,6 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
import CoreFoundation
import Foundation
import OpenCombine
@@ -203,11 +202,10 @@ extension Foundation.Timer {
func cancel() {
lock.lock()
if downstream == nil {
if downstream.take() == nil {
lock.unlock()
return
}
downstream = nil
lock.unlock()
parent?.disconnect(combineIdentifier)
}
+1 -1
View File
@@ -5,4 +5,4 @@ disabled_rules:
- explicit_acl
- explicit_top_level_acl
- explicit_enum_raw_value
- untyped_error_in_catch
@@ -0,0 +1,73 @@
//
// FutureConcurrencyTests.swift
//
//
// Created by Sergej Jaskiewicz on 12.12.2021.
//
import XCTest
#if canImport(_Concurrency) && compiler(>=5.5)
import _Concurrency
#endif
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
// swiftlint:disable:next line_length
#if !os(Windows) && !WASI && (canImport(_Concurrency) && compiler(>=5.5) || compiler(>=5.5.1)) // TEST_DISCOVERY_CONDITION
@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *)
final class FutureConcurrencyTests: XCTestCase {
func testAsyncAwaitNonThrowingSuccess() async {
var promise: Future<Int, Never>.Promise?
let future = Future<Int, Never> { promise = $0 }
let task = Task {
await future.value
}
promise?(.success(42))
let value = await task.value
XCTAssertEqual(value, 42)
}
func testAsyncAwaitThrowingSuccess() async throws {
var promise: Future<Int, TestingError>.Promise?
let future = Future<Int, TestingError> { promise = $0 }
let task = Task {
try await future.value
}
promise?(.success(42))
let value = try await task.value
XCTAssertEqual(value, 42)
}
func testAsyncAwaitThrowingFailure() async throws {
var promise: Future<Int, TestingError>.Promise?
let future = Future<Int, TestingError> { promise = $0 }
let task = Task { try await future.value }
promise?(.failure(.oops))
do {
_ = try await task.value
XCTFail("Expected an error")
} catch let error as TestingError {
XCTAssertEqual(error, .oops)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}
#endif
File diff suppressed because it is too large Load Diff
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 26.08.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Dispatch
import XCTest
@@ -259,24 +259,22 @@ final class DispatchQueueSchedulerTests: XCTestCase {
XCTAssertEqual(Stride(exactly: 2 as UInt64)?.magnitude, 2_000_000_000)
}
func testStrideFromTooMuchSecondsCrashes() {
assertCrashes {
func testStrideFromTooMuchSeconds() {
#if arch(x86_64) || arch(arm64) || arch(s390x) || arch(powerpc64) || arch(powerpc64le)
// 64-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
// 64-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
#elseif arch(i386) || arch(arm)
// 32-bit platforms
XCTAssertGreaterThan(
Stride.seconds(Double(Int.max) / 1_000_000_000 + 1).magnitude,
.max
)
// 32-bit platforms
XCTAssertEqual(
Stride.seconds(Double(Int.max) / 1_000_000_000).magnitude,
.max
)
#else
#error("This architecture isn't known. Add it to the 32-bit or 64-bit line.")
#endif
}
}
func testStrideComparable() {
@@ -443,7 +441,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
.encode(KeyedWrapper(value: stride))
let encodedString = String(decoding: encodedData, as: UTF8.self)
XCTAssertEqual(encodedString, #"{"value":{"magnitude":419872}}"#)
XCTAssertEqual(encodedString, #"{"value":{"_nanoseconds":419872}}"#)
let decodedStride = try decoder
.decode(KeyedWrapper<Stride>.self, from: encodedData)
@@ -486,11 +484,11 @@ final class DispatchQueueSchedulerTests: XCTestCase {
let main = expectation(description: "scheduled on main queue")
main.assertForOverFulfill = true
var didExecuteMainAction = false
let didExecuteMainAction = Atomic(false)
let didExecuteBackgroundAction = Atomic(false)
mainScheduler.schedule {
didExecuteMainAction = true
didExecuteMainAction.set(true)
main.fulfill()
}
@@ -501,12 +499,14 @@ final class DispatchQueueSchedulerTests: XCTestCase {
didExecuteBackgroundAction.set(true)
}
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
// Wait for the background scheduler to execute the work.
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
XCTAssertFalse(didExecuteMainAction.value,
"action should be executed asynchronously")
XCTAssertTrue(didExecuteBackgroundAction.value)
wait(for: [main], timeout: 0.1)
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 10.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -331,6 +331,7 @@ extension OperationQueueScheduler.SchedulerTimeType.Stride
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension OperationQueueScheduler: RunLoopLikeScheduler {}
private final class TestOperationQueue: OperationQueue {
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 14.12.2019.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -599,12 +599,14 @@ private func makeScheduler(_ runLoop: RunLoop) -> RunLoopScheduler {
#endif
@available(macOS 10.15, iOS 13.0, *)
protocol DateBackedSchedulerTimeType: Strideable, Codable, Hashable {
init(_ date: Date)
var date: Date { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
Comparable,
SignedNumeric,
@@ -617,6 +619,7 @@ protocol TimeIntervalBackedSchedulerStride: SchedulerTimeIntervalConvertible,
var timeInterval: TimeInterval { get }
}
@available(macOS 10.15, iOS 13.0, *)
protocol RunLoopLikeScheduler: Scheduler
where SchedulerTimeType: DateBackedSchedulerTimeType,
SchedulerTimeType.Stride: TimeIntervalBackedSchedulerStride {
@@ -628,6 +631,7 @@ extension RunLoopScheduler.SchedulerTimeType.Stride: TimeIntervalBackedScheduler
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler.SchedulerTimeType: DateBackedSchedulerTimeType {}
@available(macOS 10.15, iOS 13.0, *)
extension RunLoopScheduler: RunLoopLikeScheduler {}
#endif // !WASI
@@ -5,7 +5,7 @@
// Created by Sergej Jaskiewicz on 23.06.2020.
//
#if !WASI
#if !WASI // TEST_DISCOVERY_CONDITION
import Foundation
import XCTest
@@ -7,8 +7,6 @@
// swiftlint:disable multiline_arguments
#if !WASI
import Foundation
import XCTest
@@ -20,7 +18,7 @@ import FoundationNetworking
// swift-corelibs-foundation that were making these tests impossible to build.
//
// Those were fixed in https://github.com/apple/swift-corelibs-foundation/pull/2587.
#if canImport(Darwin) || swift(>=5.3) // TEST_DISCOVERY_CONDITION
#if canImport(Darwin) || swift(>=5.3) && !WASI // TEST_DISCOVERY_CONDITION
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
@@ -53,13 +51,14 @@ final class URLSessionTests: XCTestCase {
private let unknownError = URLError(.unknown)
func testDataTaskPublisherFromURL() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testURL)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()), testURL)
let expectedRequest = URLRequest(url: testURL)
XCTAssertEqual(publisher.request, expectedRequest)
}
func testDataTaskPublisherFromRequest() {
let publisher = makePublisher(TestURLSession(testDataTask: .init()), testRequest)
let publisher = makePublisher(TestURLSession.withTestDataTask(.create()),
testRequest)
XCTAssertEqual(publisher.request, testRequest)
}
@@ -126,8 +125,8 @@ final class URLSessionTests: XCTestCase {
}
func testRequesting() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -166,8 +165,8 @@ final class URLSessionTests: XCTestCase {
}
func testCancelAlreadyCancelled() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -192,8 +191,8 @@ final class URLSessionTests: XCTestCase {
}
func testCrashesOnZeroDemand() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
@@ -207,8 +206,8 @@ final class URLSessionTests: XCTestCase {
}
func testURLSessionSubscriptionReflection() throws {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testURL)
try testSubscriptionReflection(
description: "DataTaskPublisher",
@@ -229,8 +228,8 @@ final class URLSessionTests: XCTestCase {
_ response: URLResponse?,
_ error: Error?,
expected: [TrackingSubscriber.Event]) {
let dataTask = TestURLSessionDataTask()
let session = TestURLSession(testDataTask: dataTask)
let dataTask = TestURLSessionDataTask.create()
let session = TestURLSession.withTestDataTask(dataTask)
let publisher = makePublisher(session, testRequest)
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
publisher.subscribe(tracking)
@@ -292,16 +291,25 @@ private class TestURLSession: URLSession {
private(set) var history = [Event]()
private(set) var dataTaskCompletionHandlers: [(Data?, URLResponse?, Error?) -> Void]
private(set) var dataTaskCompletionHandlers =
[(Data?, URLResponse?, Error?) -> Void]()
private let testDataTask: TestURLSessionDataTask
private var testDataTask: TestURLSessionDataTask?
init(testDataTask: TestURLSessionDataTask) {
self.testDataTask = testDataTask
self.dataTaskCompletionHandlers = []
#if !canImport(Darwin)
super.init(configuration: .default)
static func withTestDataTask(
_ testDataTask: TestURLSessionDataTask
) -> TestURLSession {
// This dance is to avoid the deprecation warning for the URLSession
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let sessionClass = TestURLSession.self as NSObject.Type
let session = sessionClass.init() as! TestURLSession
#else
let session = TestURLSession(configuration: .default)
#endif
session.testDataTask = testDataTask
return session
}
// MARK: Testing
@@ -379,12 +387,12 @@ private class TestURLSession: URLSession {
override func dataTask(with request: URLRequest) -> URLSessionDataTask {
history.append(.dataTaskWithRequest(request))
return testDataTask
return testDataTask!
}
override func dataTask(with url: URL) -> URLSessionDataTask {
history.append(.dataTaskWithURL(url))
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -393,7 +401,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithURLAndCompletion(url))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func dataTask(
@@ -402,7 +410,7 @@ private class TestURLSession: URLSession {
) -> URLSessionDataTask {
history.append(.dataTaskWithRequestAndCompletion(request))
dataTaskCompletionHandlers.append(completionHandler)
return testDataTask
return testDataTask!
}
override func uploadTask(with request: URLRequest,
@@ -556,7 +564,18 @@ private final class TestURLSessionDataTask: URLSessionDataTask {
private(set) var history = [Event]()
override init() {}
static func create() -> TestURLSessionDataTask {
// This dance is to avoid the deprecation warning for the URLSessionDataTask
// default initializer. Believe me, I've tried to make it less ugly with
// no success.
#if canImport(Darwin)
let dataTaskClass = TestURLSessionDataTask.self as NSObject.Type
let dataTask = dataTaskClass.init() as! TestURLSessionDataTask
#else
let dataTask = TestURLSessionDataTask()
#endif
return dataTask
}
override var taskIdentifier: Int {
history.append(.taskIdentifier)
@@ -725,6 +744,4 @@ private func makePublisher(
}
#endif // OPENCOMBINE_COMPATIBILITY_TEST || !canImport(Combine)
#endif // canImport(Darwin)
#endif // !WASI
#endif
@@ -0,0 +1,55 @@
//
// AutomaticallyFinish.swift
//
//
// Created by Sergej Jaskiewicz on 08.07.2021.
//
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class AutomaticallyFinish<Output, Failure: Error> {
let subscription: CustomSubscription
let publisher: CustomPublisherBase<Output, Failure>
init() {
subscription = .init()
publisher = .init(subscription: subscription)
}
deinit {
publisher.send(completion: .finished)
}
func notify(_ value: Output) {
_ = publisher.send(value)
}
func listen(receiveCompletion: @escaping (Subscribers.Completion<Failure>) -> Void,
receiveValue: @escaping (Output) -> Void) -> AnyCancellable {
return publisher.sink(receiveCompletion: receiveCompletion,
receiveValue: receiveValue)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish: Publisher {
func receive<Downstream: Subscriber>(subscriber: Downstream)
where Downstream.Failure == Failure, Downstream.Input == Output
{
publisher.subscribe(subscriber)
}
}
@available(macOS 10.15, iOS 13.0, *)
extension AutomaticallyFinish where Failure == Never {
func assign<Root>(to keyPath: ReferenceWritableKeyPath<Root, Output>,
on object: Root) -> AnyCancellable {
return publisher.assign(to: keyPath, on: object)
}
}
@@ -214,4 +214,19 @@ func unreachable() -> Never {
fatalError("unreachable")
}
func fromNever<T>(_ resultType: T.Type) -> (Never) -> T {
// This is to avoid the 'Will never be executed' warning.
//
// The first variant doesn't produce warnings in Swift 5.1, but doesn't compile
// with early Swift versions.
//
// The second variant compiles with all Swift versions,
// but produces a warning in Swift 5.1.
#if swift(>=5.1)
return { (_: Never) -> T in }
#else
return { switch $0 {} }
#endif
}
// swiftlint:enable generic_type_name
@@ -33,8 +33,28 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}
}
private struct State {
var cancelled: Bool
var history: [Event]
}
private let state = Atomic(State(cancelled: false, history: []))
/// The history of requests and cancellations of this subscription.
private(set) var history: [Event] = []
var history: [Event] {
return state.value.history
}
var cancelled: Bool {
get {
return state.value.cancelled
}
set {
state.do { state in
state.cancelled = newValue
}
}
}
var onRequest: ((Subscribers.Demand) -> Void)?
var onCancel: (() -> Void)?
@@ -63,16 +83,18 @@ final class CustomSubscription: Subscription, CustomStringConvertible {
}.last
}
var cancelled = false
func request(_ demand: Subscribers.Demand) {
history.append(.requested(demand))
state.do { state in
state.history.append(.requested(demand))
}
onRequest?(demand)
}
func cancel() {
history.append(.cancelled)
cancelled = true
state.do { state in
state.history.append(.cancelled)
state.cancelled = true
}
onCancel?()
}
@@ -11,38 +11,76 @@
import Darwin
#elseif canImport(Glibc)
import Glibc
#elseif os(Windows)
import WinSDK
#else
#error("How to do threads on this platform?")
#endif
#if canImport(Darwin)
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t?>
#else
private typealias ThreadPtr = UnsafeMutablePointer<pthread_t>
#endif
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
func executeOnBackgroundThread<ResultType>(
_ body: () -> ResultType
) -> ResultType {
return withoutActuallyEscaping(body) { body in
typealias ThreadRoutine = () -> UnsafeMutableRawPointer
// We need this because @convention(c) closures can't capture generic params.
var typeErasedBody: () -> UnsafeMutableRawPointer = {
#if canImport(Darwin)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t?>
#elseif canImport(Glibc)
typealias ThreadHandle = UnsafeMutablePointer<pthread_t>
#elseif os(Windows)
typealias ThreadHandle = HANDLE?
#endif
let typeErasedBody: ThreadRoutine = {
let resultPtr = UnsafeMutablePointer<ResultType>.allocate(capacity: 1)
resultPtr.initialize(to: body())
return UnsafeMutableRawPointer(resultPtr)
}
return withUnsafeMutablePointer(to: &typeErasedBody) { typeErasedBody in
let _backgroundThread = ThreadPtr.allocate(capacity: 1)
var _backgroundThread: ThreadHandle
defer { _backgroundThread.deallocate() }
#if os(Windows)
typealias ResultPtr = UnsafeMutablePointer<UnsafeMutableRawPointer>
typealias Context = (ThreadRoutine, ResultPtr)
var resultPtr: ResultPtr = .allocate(capacity: 1)
defer { resultPtr.deallocate() }
_backgroundThread = nil
var context: Context = (typeErasedBody, resultPtr)
#else
_backgroundThread = .allocate(capacity: 1)
defer { _backgroundThread.deallocate() }
var context = typeErasedBody
#endif
var status: Int32 = 0
return withUnsafeMutablePointer(to: &context) { context in
#if os(Windows)
_backgroundThread = CreateThread(
nil, // default security attributes
0, // use default stack size
{ context in
let (typeErasedBody, resultPtr) = context!
.assumingMemoryBound(to: Context.self)
.pointee
// We could use Foundation's Thread, but it doesn't work on Linux for some
// reason.
status = pthread_create(
resultPtr.initialize(to: typeErasedBody())
return 0
},
context,
0, // use default creation flags
nil // don't return thread identifier
)
precondition(_backgroundThread != nil, "Could not create a thread")
WaitForSingleObject(_backgroundThread!, INFINITE)
defer { resultPtr.pointee.deallocate() }
return resultPtr.pointee.assumingMemoryBound(to: ResultType.self).move()
#else
var status = pthread_create(
_backgroundThread,
nil,
{ context in
@@ -52,19 +90,17 @@ func executeOnBackgroundThread<ResultType>(
let context = context!
#endif
return context
.assumingMemoryBound(to: (() -> UnsafeMutableRawPointer).self)
.assumingMemoryBound(to: ThreadRoutine.self)
.pointee()
},
typeErasedBody
context
)
guard status == 0 else {
preconditionFailure("Could not create a background thread")
}
precondition(status == 0, "Could not create a thread")
#if canImport(Darwin)
guard let backgroundThread = _backgroundThread.pointee else {
preconditionFailure("Could not create a background thread")
preconditionFailure("Could not join thread")
}
#else
let backgroundThread = _backgroundThread.pointee
@@ -74,12 +110,13 @@ func executeOnBackgroundThread<ResultType>(
status = pthread_join(backgroundThread, &_resultPtr)
guard status == 0, let resultPtr = _resultPtr else {
preconditionFailure("Could not join threads")
preconditionFailure("Could not join thread")
}
defer { resultPtr.deallocate() }
return resultPtr.assumingMemoryBound(to: ResultType.self).move()
#endif
}
}
}
@@ -352,6 +352,7 @@ enum StringSubscription: Subscription,
ExpressibleByStringLiteral {
case string(String)
case contains(String)
case subscription(Subscription)
init(_ subscription: Subscription) {
@@ -364,7 +365,7 @@ enum StringSubscription: Subscription,
var description: String {
switch self {
case .string(let string):
case .string(let string), .contains(let string):
return string
case .subscription(let subscription):
return String(describing: subscription)
@@ -379,7 +380,7 @@ enum StringSubscription: Subscription,
switch self {
case .subscription(let subscription):
return subscription.combineIdentifier
case .string:
case .string, .contains:
fatalError("String has no combineIdentifier")
}
}
@@ -390,7 +391,7 @@ enum StringSubscription: Subscription,
var underlying: Subscription? {
switch self {
case .string:
case .string, .contains:
return nil
case .subscription(let underlying):
return underlying
@@ -401,6 +402,25 @@ enum StringSubscription: Subscription,
@available(macOS 10.15, iOS 13.0, *)
extension StringSubscription: Equatable {
static func == (lhs: StringSubscription, rhs: StringSubscription) -> Bool {
return lhs.description == rhs.description
// swiftlint:disable pattern_matching_keywords
switch (lhs, rhs) {
case (.contains(let pattern), .subscription(let subscription)),
(.subscription(let subscription), .contains(let pattern)):
return String(describing: subscription).contains(pattern)
case (.contains(let pattern), .string(let string)),
(.string(let string), .contains(let pattern)):
return string.contains(pattern)
case let (.subscription(lhs), .subscription(rhs)):
return String(describing: lhs) == String(describing: rhs)
case (.string(let string), .subscription(let subscription)),
(.subscription(let subscription), .string(let string)):
return String(describing: subscription) == string
case let (.string(lhs), .string(rhs)):
return lhs == rhs
case let (.contains(lhs), .contains(rhs)):
return lhs.contains(rhs) || rhs.contains(lhs)
}
// swiftlint:enable pattern_matching_keywords
}
}
@@ -357,9 +357,9 @@ final class VirtualTimeScheduler: Scheduler {
_now = time
}
func executeScheduledActions(until deadline: SchedulerTimeType = .nanoseconds(.max)) {
precondition(deadline >= _now)
while let (time, action) = workQueue.min(), time <= deadline {
func executeScheduledActions(until deadline: SchedulerTimeType? = nil) {
precondition(deadline.map { $0 >= _now } ?? true)
while let (time, action) = workQueue.min(), deadline.map({ time <= $0 }) ?? true {
workQueue.extractMin()
_now = max(time, _now)
action()
@@ -41,14 +41,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "AllSatisfy",
expectedResult: true,
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "AllSatisfy",
{ $0.allSatisfy(shouldNotBeCalled()) }
)
}
func testAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.allSatisfy(shouldNotBeCalled())
@@ -104,6 +111,7 @@ final class AllSatisfyTests: XCTestCase {
func testAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.allSatisfy { _ in true } })
}
@@ -155,14 +163,21 @@ final class AllSatisfyTests: XCTestCase {
)
}
func testTryAllSatisfyUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(
func testTryAllSatisfyUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryAllSatisfy",
expectedResult: true,
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryAllSatisfy",
{ $0.tryAllSatisfy(shouldNotBeCalled()) }
)
}
func testTryAllSatisfyCancelAlreadyCancelled() throws {
try ReduceTests.testCancelAlreadyCancelled {
$0.tryAllSatisfy(shouldNotBeCalled())
@@ -224,6 +239,7 @@ final class AllSatisfyTests: XCTestCase {
func testTryAllSatisfyLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryAllSatisfy { _ in true } })
}
@@ -46,9 +46,11 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [])
shouldStop = true
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(subscription: CustomSubscription())
}
#endif
}
func testReceiveValue() {
@@ -77,9 +79,11 @@ final class BreakpointTests: XCTestCase {
.subscription("CustomSubscription")])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
_ = helper.publisher.send(-1)
}
#endif
}
func testReceiveCompletion() {
@@ -107,9 +111,11 @@ final class BreakpointTests: XCTestCase {
.value(21),
.subscription("CustomSubscription")])
XCTAssertEqual(counter, 2)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .finished)
}
#endif
}
func testBreakpointOnError() throws {
@@ -139,9 +145,11 @@ final class BreakpointTests: XCTestCase {
XCTAssertEqual(helper.sut.receiveCompletion?(.finished), false)
XCTAssertEqual(helper.sut.receiveCompletion?(.failure(.oops)), true)
#if !os(Windows)
assertCrashes {
helper.publisher.send(completion: .failure(.oops))
}
#endif
}
func testCancelAlreadyCancelled() throws {
@@ -63,7 +63,7 @@ final class CatchTests: XCTestCase {
func testCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.catch { _ in Just(13) }
$0.catch(fromNever(Just<Int>.self))
}
}
@@ -71,7 +71,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.catch { _ in Just(13) } }
{ $0.catch(fromNever(Just<Int>.self)) }
)
}
@@ -220,7 +220,7 @@ final class CatchTests: XCTestCase {
func testTryCatchReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 1, expected: .crash) {
$0.tryCatch { _ in Just(13) }
$0.tryCatch(fromNever(Just<Int>.self))
}
}
@@ -228,7 +228,7 @@ final class CatchTests: XCTestCase {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.tryCatch { _ in Just(13) } }
{ $0.tryCatch(fromNever(Just<Int>.self)) }
)
}
@@ -27,10 +27,19 @@ final class CollectTests: XCTestCase {
{ $0.collect() })
}
func testtestUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
expectedResult: [Int](),
{ $0.collect() })
func testUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Collect",
expectedResult: [Int](),
{ $0.collect() }
)
}
func testUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Collect",
{ $0.collect() }
)
}
func testCancelAlreadyCancelled() throws {
@@ -77,6 +86,7 @@ final class CollectTests: XCTestCase {
func testCollectLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.collect() })
}
@@ -64,22 +64,52 @@ final class ComparisonTests: XCTestCase {
{ $0.min(by: shouldNotBeCalled()) })
}
func testComparisonUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max() })
func testComparisonUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max() }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min() })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min() }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max(by: shouldNotBeCalled()) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.max(by: shouldNotBeCalled()) }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min(by: shouldNotBeCalled()) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Comparison",
expectedResult: nil,
{ $0.min(by: shouldNotBeCalled()) }
)
}
func testComparisonUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.max() }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.min() }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.max(by: shouldNotBeCalled()) }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Comparison",
{ $0.min(by: shouldNotBeCalled()) }
)
}
func testComparisonCancelAlreadyCancelled() throws {
@@ -204,10 +234,12 @@ final class ComparisonTests: XCTestCase {
func testComparisonLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.min(by: >) })
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.max(by: >) })
}
@@ -272,14 +304,30 @@ final class ComparisonTests: XCTestCase {
{ $0.tryMin(by: >) })
}
func testTryComparisonUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMax(by: >) })
func testTryComparisonUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMax(by: >) }
)
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMin(by: >) })
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryComparison",
expectedResult: nil,
{ $0.tryMin(by: >) }
)
}
func testTryComparisonUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryComparison",
{ $0.tryMax(by: >) }
)
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryComparison",
{ $0.tryMin(by: >) }
)
}
func testTryComparisonCancelAlreadyCancelled() throws {
@@ -358,10 +406,12 @@ final class ComparisonTests: XCTestCase {
func testTryComparisonLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryMin(by: >) })
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryMax(by: >) })
}
@@ -377,6 +377,60 @@ final class ConcatenateTests: XCTestCase {
XCTAssertEqual(publisher.prepend(CollectionOfOne(42)).prefix.sequence.first, 42)
}
func testReleasesSuffixOnCancellation() throws {
var suffixIsDestroyed = false
var downstreamSubscription: Subscription?
do {
let prefix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
let suffix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
suffix.onDeinit = {
suffixIsDestroyed = true
}
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
}
)
prefix.append(suffix).subscribe(tracking)
}
XCTAssertFalse(suffixIsDestroyed)
try XCTUnwrap(downstreamSubscription).cancel()
XCTAssertTrue(suffixIsDestroyed)
}
func testReceiveCompletionWhileCancelling() throws {
var downstreamSubscription: Subscription?
do {
let prefix =
CustomPublisherBase<Int, Never>(subscription: CustomSubscription())
let autofinish = AutomaticallyFinish<Int, Never>()
let tracking = TrackingSubscriberBase<Int, Never>(
receiveSubscription: {
downstreamSubscription = $0
}
)
prefix.append(autofinish).subscribe(tracking)
prefix.send(completion: .finished)
}
// autofinish is deallocated here, a completion is sent
try XCTUnwrap(downstreamSubscription).cancel()
}
func testAppendReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 12,
@@ -41,11 +41,19 @@ final class ContainsTests: XCTestCase {
{ $0.contains(0) })
}
func testContainsUpstreamFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "Contains",
expectedResult: false,
{ $0.contains(0) })
func testContainsUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Contains",
expectedResult: false,
{ $0.contains(0) }
)
}
func testContainsUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Contains",
{ $0.contains(0) }
)
}
func testContainsCancelAlreadyCancelled() throws {
@@ -99,6 +107,7 @@ final class ContainsTests: XCTestCase {
func testContainsLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.contains(31) })
}
@@ -142,12 +151,18 @@ final class ContainsTests: XCTestCase {
)
}
func testContainsWhereUpstreamFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(
expectedSubscription: "ContainsWhere",
expectedResult: false,
{ $0.contains(where: shouldNotBeCalled()) }
func testContainsWhereUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "ContainsWhere",
expectedResult: false,
{ $0.contains(where: shouldNotBeCalled()) }
)
}
func testContainsWhereUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "ContainsWhere",
{ $0.contains(where: shouldNotBeCalled()) }
)
}
@@ -206,6 +221,7 @@ final class ContainsTests: XCTestCase {
func testContainsWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.contains { _ in true } })
}
@@ -264,11 +280,19 @@ final class ContainsTests: XCTestCase {
)
}
func testTryContainsWhereUpstreamFinishesImmediately() {
ReduceTests .testUpstreamFinishesImmediately(
func testTryContainsWhereUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryContainsWhere",
expectedResult: false,
{ $0.tryContains(where: shouldNotBeCalled()) })
{ $0.tryContains(where: shouldNotBeCalled()) }
)
}
func testTryContainsWhereUpstreamFinishesImmediatelyWithWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryContainsWhere",
{ $0.tryContains(where: shouldNotBeCalled()) }
)
}
func testTryContainsWhereCancelAlreadyCancelled() throws {
@@ -332,6 +356,7 @@ final class ContainsTests: XCTestCase {
func testTryContainsWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryContains { _ in true } })
}
@@ -27,10 +27,19 @@ final class CountTests: XCTestCase {
{ $0.count() })
}
func testtestUpstreamFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Count",
expectedResult: 0,
{ $0.count() })
func testUpstreamFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Count",
expectedResult: 0,
{ $0.count() }
)
}
func testUpstreamFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Count",
{ $0.count() }
)
}
func testCancelAlreadyCancelled() throws {
@@ -77,6 +86,7 @@ final class CountTests: XCTestCase {
func testCountLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.count() })
}
@@ -77,11 +77,19 @@ final class FirstTests: XCTestCase {
}
}
func testFirstFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "First",
expectedResult: nil) {
$0.first()
}
func testFirstFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "First",
expectedResult: nil,
{ $0.first() }
)
}
func testFirstFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "First",
{ $0.first() }
)
}
func testFirstRequestsUnlimitedThenSendsSubscription() {
@@ -125,6 +133,7 @@ final class FirstTests: XCTestCase {
func testFirstLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.first() })
}
@@ -222,11 +231,19 @@ final class FirstTests: XCTestCase {
}
}
func testFirstWhereFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryFirst",
expectedResult: nil) {
$0.first(where: { $0 > 2 })
}
func testFirstWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryFirst",
expectedResult: nil,
{ $0.first(where: { $0 > 2 }) }
)
}
func testFirstWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryFirst",
{ $0.first(where: { $0 > 2 }) }
)
}
func testFirstWhereRequestsUnlimitedThenSendsSubscription() {
@@ -278,6 +295,7 @@ final class FirstTests: XCTestCase {
func testFirstWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.first { $0 > 1 } })
}
@@ -369,12 +387,19 @@ final class FirstTests: XCTestCase {
}
}
func testTryFirstWhereFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "TryFirstWhere",
expectedResult: nil) {
$0.tryFirst(where: { $0 > 2 })
}
func testTryFirstWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryFirstWhere",
expectedResult: nil,
{ $0.tryFirst(where: { $0 > 2 }) }
)
}
func testTryFirstWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryFirstWhere",
{ $0.tryFirst(where: { $0 > 2 }) }
)
}
func testTryFirstWhereRequestsUnlimitedThenSendsSubscription() {
@@ -446,6 +471,7 @@ final class FirstTests: XCTestCase {
func testTryFirstWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryFirst { $0 > 1 } })
}
@@ -589,7 +589,7 @@ final class FlatMapTests: XCTestCase {
createSut: { $0.flatMap(maxPublishers: .max(1)) { $0 } }
)
child.willSubscribe = { subscriber, _ in
child.willSubscribe = { _, _ in
helper.publisher.send(completion: .finished)
}
@@ -231,7 +231,7 @@ final class FutureTests: XCTestCase {
XCTAssertTrue(hasStarted)
}
func testWaitsForDemandSuccess() {
func testWaitsForDemandSuccess() throws {
var promise: Sut.Promise?
let future = Sut { promise = $0 }
@@ -248,13 +248,22 @@ final class FutureTests: XCTestCase {
.subscription("Future")
])
downstreamSubscription?.request(.max(1))
let unwrappedDownstreamSubscription = try XCTUnwrap(downstreamSubscription)
unwrappedDownstreamSubscription.request(.max(1))
XCTAssertEqual(subscriber.history, [
.subscription("Future"),
.value(42),
.completion(.finished)
])
let parent = try XCTUnwrap(
Mirror(reflecting: unwrappedDownstreamSubscription)
.descendant("parent") as? Sut?
)
XCTAssertNotNil(parent)
}
func testReleasesEverythingOnTermination() {
@@ -315,5 +324,17 @@ final class FutureTests: XCTestCase {
playgroundDescription: "Future",
sut: Sut { _ in }
)
try testSubscriptionReflection(
description: "Future",
customMirror: expectedChildren(
("parent", "nil"),
("downstream", "nil"),
("hasAnyDemand", "false"),
("subject", "nil")
),
playgroundDescription: "Future",
sut: Sut { promise in promise(.failure(.oops)) }
)
}
}
@@ -256,7 +256,10 @@ final class JustTests: XCTestCase {
}
func testMapErrorOperatorSpecialization() {
XCTAssertEqual(try Sut(42).mapError { _ in TestingError.oops }.result.get(), 42)
XCTAssertEqual(
try Sut(42).mapError(fromNever(TestingError.self)).result.get(),
42
)
}
func testReplaceErrorOperatorSpecialization() {
@@ -49,11 +49,19 @@ final class LastTests: XCTestCase {
}
}
func testLastFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Last",
expectedResult: nil) {
$0.last()
}
func testLastFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Last",
expectedResult: nil,
{ $0.last() }
)
}
func testLastFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Last",
{ $0.last() }
)
}
func testLastRequestsUnlimitedThenSendsSubscription() {
@@ -97,6 +105,7 @@ final class LastTests: XCTestCase {
func testLastLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.last() })
}
@@ -195,11 +204,19 @@ final class LastTests: XCTestCase {
}
}
func testLastWhereFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "LastWhere",
expectedResult: nil) {
$0.last(where: { $0 > 2 })
}
func testLastWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "LastWhere",
expectedResult: nil,
{ $0.last(where: { $0 > 2 }) }
)
}
func testLastWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "LastWhere",
{ $0.last(where: { $0 > 2 }) }
)
}
func testLastWhereRequestsUnlimitedThenSendsSubscription() {
@@ -251,6 +268,7 @@ final class LastTests: XCTestCase {
func testLastWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.last { $0 > 1 } })
}
@@ -350,12 +368,19 @@ final class LastTests: XCTestCase {
}
}
func testTryLastWhereFinishesImmediately() {
ReduceTests
.testUpstreamFinishesImmediately(expectedSubscription: "TryLastWhere",
expectedResult: nil) {
$0.tryLast(where: { $0 > 2 })
}
func testTryLastWhereFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryLastWhere",
expectedResult: nil,
{ $0.tryLast(where: { $0 > 2 }) }
)
}
func testTryLastWhereFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryLastWhere",
{ $0.tryLast(where: { $0 > 2 }) }
)
}
func testTryLastWhereRequestsUnlimitedThenSendsSubscription() {
@@ -427,6 +452,7 @@ final class LastTests: XCTestCase {
func testTryLastWhereLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryLast { $0 > 1 } })
}
@@ -338,8 +338,6 @@ final class OptionalPublisherTests: XCTestCase {
XCTAssertEqual(Sut<Int>(12).output(in: ...0), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: ..<0), Sut(nil))
XCTAssertEqual(Sut<Int>(12).output(in: ..<1), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (0, -1))), Sut(12))
XCTAssertEqual(Sut<Int>(12).output(in: Range(uncheckedBounds: (1, -1))), Sut(nil))
let trackingRange = TrackingRangeExpression(0 ..< 10)
_ = Sut<Int>(12).output(in: trackingRange)
@@ -0,0 +1,386 @@
//
// PrefixUntilOutputTests.swift
//
//
// Created by Sergej Jaskiewicz on 08.11.2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class PrefixUntilOutputTests: XCTestCase {
func testBasicBehavior() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput"))])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.publisher.send(1), .max(2))
XCTAssertEqual(helper.publisher.send(2), .max(2))
XCTAssertEqual(terminatingPublisher.send(1000), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(terminatingPublisher.send(1001), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
helper.publisher.send(completion: .finished)
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.value(1),
.value(2),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.max(3)), .cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testCombineIdentifiers() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: .max(3),
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingPublisher.subscriber?.combineIdentifier,
helper.publisher.subscriber?.combineIdentifier)
}
func testRequestZeroDemand() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
try XCTUnwrap(helper.downstreamSubscription).request(.none)
XCTAssertEqual(helper.subscription.history, [.requested(.none)])
}
func testCancellation() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
terminatingSubscription.onCancel = {
XCTAssertEqual(helper.subscription.history,
[.cancelled],
"Upstream subscription should be cancelled first")
}
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history,
[.requested(.max(1)), .cancelled])
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history,
[.requested(.max(1)), .cancelled])
}
func testUpstreamCompletion() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
helper.tracking.onFinish = {
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
.cancelled])
}
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)),
.cancelled])
}
func testCancelsUpstreamWhenTerminatorSendsValue() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
helper.tracking.onFinish = {
XCTAssertEqual(helper.subscription.history, [.cancelled])
}
XCTAssertEqual(terminatingPublisher.send(42), .none)
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.cancelled])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testTerminatorFinishesWithoutProducingValues() {
testTerminatorCompletesWithoutProducingValues(completion: .finished)
}
func testTerminatorFailsWithoutProducingValues() {
testTerminatorCompletesWithoutProducingValues(completion: .failure(.oops))
}
private func testTerminatorCompletesWithoutProducingValues(
completion: Subscribers.Completion<TestingError>
) {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .max(2),
createSut: { $0.prefix(untilOutputFrom: terminatingPublisher) }
)
terminatingPublisher.send(completion: completion)
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput"))])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(terminatingPublisher.send(42), .none)
terminatingPublisher.send(completion: .finished)
terminatingPublisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.publisher.send(1), .max(2))
XCTAssertEqual(helper.subscription.history, [])
XCTAssertEqual(helper.tracking.history,
[.subscription(.contains("PrefixUntilOutput")), .value(1)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testTerminatorEmitsValueBeforeUpstreamSendsSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
let upstream = CustomPublisher(subscription: nil)
let tracking = TrackingSubscriber()
upstream.prefix(untilOutputFrom: terminatingPublisher).subscribe(tracking)
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
XCTAssertEqual(terminatingPublisher.send(-1), .none)
XCTAssertEqual(tracking.history, [.completion(.finished)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
let subscription = CustomSubscription()
upstream.send(subscription: subscription)
XCTAssertEqual(subscription.history, [.cancelled])
XCTAssertEqual(tracking.history, [.completion(.finished)])
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testPrefixUntilOutputReceiveValueBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testReceiveValueBeforeSubscription(
value: 31,
expected: .history([], demand: .none),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
testReceiveValueBeforeSubscription(
value: 31,
expected: .history([.subscription(.contains("PrefixUntilOutput"))],
demand: .none),
{ publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(subscription.history, [])
}
func testPrefixUntilOutputReceiveCompletionBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([]),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.subscription(.contains("PrefixUntilOutput"))]),
{ publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(subscription.history, [])
}
func testPrefixUntilOutputRequestBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testRequestBeforeSubscription(
inputType: Int.self,
shouldCrash: false,
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1))])
}
func testPrefixUntilOutputCancelBeforeSubscription() {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
testCancelBeforeSubscription(
inputType: Int.self,
expected: .history([.cancelled]),
{ $0.prefix(untilOutputFrom: terminatingPublisher) }
)
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
}
func testPrefixUntilOutputReceiveSubscriptionTwice() throws {
let terminatingSubscription = CustomSubscription()
let terminatingPublisher = CustomPublisher(subscription: terminatingSubscription)
try testReceiveSubscriptionTwice {
$0.prefix(untilOutputFrom: terminatingPublisher)
}
XCTAssertEqual(terminatingSubscription.history, [.requested(.max(1)), .cancelled])
do {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { publisher.prefix(untilOutputFrom: $0) }
)
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)),
.cancelled,
.cancelled])
XCTAssertEqual(subscription.history, [.cancelled])
}
}
func testPrefixUntilOutputReflection() throws {
// PrefixUntilOutput's Inner doesn't customize its reflection
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: nil,
customMirror: nil,
playgroundDescription: nil) {
$0.prefix(untilOutputFrom: terminatingPublisher)
}
let publisher = CustomPublisher(subscription: CustomSubscription())
try testReflection(parentInput: Int.self,
parentFailure: Error.self,
description: nil,
customMirror: nil,
playgroundDescription: nil) {
publisher.prefix(untilOutputFrom: $0)
}
}
func testPrefixUntilOutputLifecycle() throws {
let terminatingPublisher = CustomPublisher(subscription: CustomSubscription())
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.prefix(untilOutputFrom: terminatingPublisher) })
let publisher = CustomPublisher(subscription: CustomSubscription())
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ publisher.prefix(untilOutputFrom: $0) })
}
}
@@ -30,11 +30,19 @@ final class ReduceTests: XCTestCase {
}
}
func testReduceFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Reduce",
expectedResult: 1) {
$0.reduce(1, *)
}
func testReduceFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "Reduce",
expectedResult: 1,
{ $0.reduce(1, *) }
)
}
func testReduceFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "Reduce",
{ $0.reduce(1, *) }
)
}
func testReduceRequestsUnlimitedThenSendsSubscription() {
@@ -78,6 +86,7 @@ final class ReduceTests: XCTestCase {
func testReduceLifecycle() throws {
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.reduce(0, +) })
}
@@ -118,11 +127,19 @@ final class ReduceTests: XCTestCase {
}
}
func testTryReduceFinishesImmediately() {
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryReduce",
expectedResult: 1) {
$0.tryReduce(1, *)
}
func testTryReduceFinishesImmediatelyWithDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithDemand(
expectedSubscription: "TryReduce",
expectedResult: 1,
{ $0.tryReduce(1, *) }
)
}
func testTryReduceFinishesImmediatelyWithoutDemand() {
ReduceTests.testUpstreamFinishesImmediatelyWithoutDemand(
expectedSubscription: "TryReduce",
{ $0.tryReduce(1, *) }
)
}
func testTryReduceRequestsUnlimitedThenSendsSubscription() {
@@ -172,6 +189,7 @@ final class ReduceTests: XCTestCase {
func testTryReduceLifecycle() throws {
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
finishingIsPassedThrough: false,
{ $0.tryReduce(0, +) })
}
@@ -306,7 +324,7 @@ final class ReduceTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testUpstreamFinishesImmediately<Operator: Publisher>(
static func testUpstreamFinishesImmediatelyWithDemand<Operator: Publisher>(
expectedSubscription: StringSubscription,
expectedResult: Operator.Output?,
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
@@ -314,7 +332,7 @@ final class ReduceTests: XCTestCase {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<Int, Error>.self,
initialDemand: nil, // Downstream should receive the result nonetheless
initialDemand: .max(1),
receiveValueDemand: .none,
createSut: makeOperator
)
@@ -344,6 +362,38 @@ final class ReduceTests: XCTestCase {
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testUpstreamFinishesImmediatelyWithoutDemand<Operator: Publisher>(
expectedSubscription: StringSubscription,
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
) where Operator.Output: Equatable, Operator.Failure == Error {
let helper = OperatorTestHelper(
publisherType: CustomPublisherBase<Int, Error>.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: makeOperator
)
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .finished)
let expectedHistory: [TrackingSubscriberBase<Operator.Output, Error>.Event] =
[.subscription(expectedSubscription)]
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
helper.publisher.send(completion: .failure(TestingError.oops))
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.publisher.send(73), .none)
XCTAssertEqual(helper.tracking.history, expectedHistory)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
static func testCancelAlreadyCancelled<Operator: Publisher>(
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
) throws where Operator.Output: Equatable, Operator.Failure == Error {
@@ -68,7 +68,7 @@ final class ReplaceErrorTests: XCTestCase {
.completion(.finished)])
}
func testSendingErrorWithNoDemandThenFinish() {
func testSendingErrorWithNoDemandThenFinish() throws {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
@@ -77,7 +77,12 @@ final class ReplaceErrorTests: XCTestCase {
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError")])
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
XCTAssertEqual(helper.tracking.history, [.subscription("ReplaceError"),
.value(42),
.completion(.finished)])
}
@@ -164,7 +169,7 @@ final class ReplaceErrorTests: XCTestCase {
replaceError.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("ReplaceError")])
XCTAssertEqual(tracking.history, [])
let subscription = CustomSubscription()
publisher.send(subscription: subscription)
@@ -225,7 +230,7 @@ final class ReplaceErrorTests: XCTestCase {
func testReplaceErrorReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(
value: 0,
expected: .history([.subscription("ReplaceError")], demand: .none),
expected: .history([], demand: .none),
{ $0.replaceError(with: 1) }
)
}
@@ -233,7 +238,7 @@ final class ReplaceErrorTests: XCTestCase {
func testReplaceErrorCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(
inputType: Int.self,
expected: .history([.subscription("ReplaceError")]),
expected: .history([]),
{ $0.replaceError(with: 1) }
)
}
@@ -0,0 +1,844 @@
//
// ThrottleTests.swift
//
//
// Created by Stuart Austin on 14/11/2020.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ThrottleTests: XCTestCase {
func testBasicBehavior() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .max(100),
receiveValueDemand: .max(12)) {
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: true)
}
let helper = extractedExpr
XCTAssertNotNil(helper.publisher.subscriber,
"Subscription must be performed synchronously")
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
.now]) // Subscription received by Subscriber
// Send an initial value to the subject. This should be scheduled immediately
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
// Checking the time when the Subscriber
// receives the input "1"
.now,
// Scheduling the output
// of the input immediately as we have not
// output any values
.schedule(options: nil)
])
// Send some more values to the subject. Since we haven't run the scheduled
// output above, these won't create any additional scheduled work
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
// Checking the time when the Subscriber
// receives the input "2"
.now,
// Checking the time when the Subscriber
// receives the input "3"
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
// Expect only "3" to be output
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now]) // Log the time of the output
// Send another value to the subject. This should be scheduled after the interval
XCTAssertEqual(helper.publisher.send(4), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "4"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil)])
helper.publisher.send(completion: .failure(.oops))
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.publisher.send(5), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(3),
.value(4),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now])
XCTAssertEqual(scheduler.now, .seconds(1337))
}
func testThrottleDemand() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .max(2),
receiveValueDemand: .none) {
$0.throttle(for: .seconds(1337), scheduler: scheduler, latest: false)
}
let helper = extractedExpr
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, // Subscriber created
.now]) // Subscription received by Subscriber
// Send an initial value to the subject. This should be scheduled immediately
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
// Checking the time when the Subscriber
// receives the input "1"
.now,
// Scheduling the output of the input
// immediately as we have not output any values
.schedule(options: nil)])
// Send some more values to the subject.
// Since we haven't run the scheduled output above, these won't create
// any additional scheduled work
XCTAssertEqual(helper.publisher.send(5), .none)
XCTAssertEqual(helper.publisher.send(6), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
// Checking the time when the Subscriber
// receives the input "5"
.now,
// Checking the time when the Subscriber
// receives the input "6"
.now])
scheduler.executeScheduledActions()
// Send a second value to the subject. This should be scheduled after the interval
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.seconds(1337)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "2"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
// Send a third value to the subject.
// This should not be output at all due to the demand
XCTAssertEqual(helper.publisher.send(3), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
// Checking the time when the Subscriber
// receives the input "4"
.now,
// When scheduling the output, it uses
// the minimum tolerance
.minimumTolerance,
// Scheduling of the output
.scheduleAfterDate(.seconds(1337),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(1),
.value(2)])
}
func testThrottleGap() {
let scheduler = VirtualTimeScheduler()
let extractedExpr = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: false)
}
let helper = extractedExpr
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now])
XCTAssertEqual(helper.publisher.send(0), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [.nanoseconds(0)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
var future = scheduler.now + .seconds(45)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now])
// change the current time to be 45 seconds into the future
scheduler.rewind(to: future)
XCTAssertEqual(helper.publisher.send(1), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"), .value(0)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
// next value should be emitted 60 seconds from the start of time
XCTAssertEqual(scheduler.scheduledDates, [.seconds(60)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
future = scheduler.now + .seconds(61)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now])
// change the current time to be 61 seconds into the future
scheduler.rewind(to: future)
XCTAssertEqual(helper.publisher.send(2), .none)
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now,
.now,
// next value should be scheduled immediately
// as the interval has passed
.schedule(options: nil)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
// next value should be emitted 121 seconds from the start of time
XCTAssertEqual(scheduler.scheduledDates, [.seconds(121)])
scheduler.executeScheduledActions()
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now,
.now,
.now,
.schedule(options: nil),
.now])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(1),
.value(2)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.scheduledDates, [])
}
func testRequest() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
}
scheduler.executeScheduledActions()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
try XCTUnwrap(helper.downstreamSubscription).request(.max(10))
try XCTUnwrap(helper.downstreamSubscription).request(.max(4))
try XCTUnwrap(helper.downstreamSubscription).request(.max(5))
try XCTUnwrap(helper.downstreamSubscription).request(.none)
XCTAssertEqual(helper.publisher.send(2000), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(2000)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testCancelAlreadyCancelled() throws {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(10), scheduler: scheduler, latest: true)
}
scheduler.executeScheduledActions()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
try XCTUnwrap(helper.downstreamSubscription).cancel()
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(helper.publisher.send(0), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now])
}
func testNoDemandReceivesNoValues() throws {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
let tracking = TrackingSubscriber(
receiveValue: { _ in
XCTFail("Unexpected value received")
return .none
}
)
let throttle = publisher.throttle(for: .milliseconds(1),
scheduler: ImmediateScheduler.shared,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
}
func testCancelWhileReceivingInput() throws {
let scheduler = VirtualTimeScheduler()
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.unlimited)
},
receiveValue: { _ in
XCTAssertNotNil(downstreamSubscription)
downstreamSubscription?.cancel()
return .max(42)
}
)
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle"), .value(1)])
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now])
}
func testCancelWhilstScheduledOutput() {
let scheduler = VirtualTimeScheduler()
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
var downstreamSubscription: Subscription?
let tracking = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(.unlimited)
},
receiveValue: { _ in
XCTAssertNotNil(downstreamSubscription)
return .max(42)
}
)
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
throttle.subscribe(tracking)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now])
XCTAssertEqual(publisher.send(1), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [])
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil)])
}
func testReceiveCompletionImmediatelyAfterSubscription() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
}
helper.publisher.send(completion: .failure(.oops))
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle")])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.completion(.failure(.oops))])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testReceiveCompletionImmediatelyAfterValue() {
let scheduler = VirtualTimeScheduler()
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .max(418)) {
$0.throttle(for: .seconds(60), scheduler: scheduler, latest: true)
}
XCTAssertEqual(helper.publisher.send(-1), .none)
scheduler.executeScheduledActions()
XCTAssertEqual(helper.publisher.send(1000), .none)
helper.publisher.send(completion: .finished)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(-1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
XCTAssertEqual(scheduler.history, [.now,
.now,
.now,
.schedule(options: nil),
.now,
.now,
.minimumTolerance,
.scheduleAfterDate(.seconds(60),
tolerance: .nanoseconds(7),
options: nil),
.now])
scheduler.executeScheduledActions()
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(-1),
.value(1000),
.completion(.finished)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testReceiveInputRecursively() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .max(418)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
var recursionCounter = 5
helper.tracking.onValue = { _ in
if recursionCounter == 0 { return }
recursionCounter -= 1
_ = helper.publisher.send(-1)
}
XCTAssertEqual(helper.publisher.send(0), .none)
XCTAssertEqual(helper.tracking.history, [.subscription("Throttle"),
.value(0),
.value(-1),
.value(-1),
.value(-1),
.value(-1),
.value(-1)])
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
}
func testReceiveCompletionRecursively() {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: .unlimited,
receiveValueDemand: .max(418)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
helper.tracking.onFinish = {
helper.publisher.send(completion: .finished)
}
helper.publisher.send(completion: .finished)
}
func testWeakCaptureWhenSchedulingValue() {
let scheduler = VirtualTimeScheduler()
var value: Int?
var subscriberReleased = false
do {
let publisher = CustomPublisher(subscription: CustomSubscription())
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
let tracking =
TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) },
receiveValue: { value = $0; return .none },
onDeinit: { subscriberReleased = true })
throttle.subscribe(tracking)
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(publisher.send(42), .none)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
publisher.cancel()
}
XCTAssertTrue(subscriberReleased)
XCTAssertNil(value)
}
func testWeakCaptureWhenSchedulingCompletion() {
let scheduler = VirtualTimeScheduler()
var completion: Subscribers.Completion<TestingError>?
var subscriberReleased = false
do {
let publisher = CustomPublisher(subscription: CustomSubscription())
let throttle = publisher.throttle(for: .seconds(60),
scheduler: scheduler,
latest: true)
let tracking = TrackingSubscriber(receiveCompletion: { completion = $0 },
onDeinit: { subscriberReleased = true })
throttle.subscribe(tracking)
scheduler.executeScheduledActions()
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
publisher.send(completion: .finished)
XCTAssertEqual(tracking.history, [.subscription("Throttle")])
XCTAssertEqual(scheduler.history, [.now, .now, .now, .schedule(options: nil)])
tracking.cancel()
publisher.cancel()
}
XCTAssertTrue(subscriberReleased)
XCTAssertNil(completion)
scheduler.executeScheduledActions()
XCTAssertNil(completion)
}
func testThrottleReceiveSubscriptionTwice() throws {
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
.cancelled,
.cancelled])
}
func testThrottleReceiveValueBeforeSubscription() {
testReceiveValueBeforeSubscription(value: 213,
expected: .history([], demand: .none)) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleReceiveCompletionBeforeSubscription() {
testReceiveCompletionBeforeSubscription(inputType: Int.self,
expected: .history([])) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleRequestBeforeSubscription() {
testRequestBeforeSubscription(inputType: Int.self, shouldCrash: false) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleCancelBeforeSubscription() {
testCancelBeforeSubscription(inputType: Int.self,
expected: .history([.cancelled])) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
func testThrottleReflection() throws {
try testReflection(parentInput: String.self,
parentFailure: Error.self,
description: "Throttle",
customMirror: childrenIsEmpty,
playgroundDescription: "Throttle",
{ $0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true) })
}
func testThrottleLifecycle() throws {
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: true) {
$0.throttle(for: .seconds(60),
scheduler: ImmediateScheduler.shared,
latest: true)
}
}
}
@@ -0,0 +1,743 @@
//
// ZipTests.swift
//
// Created by Eric Patey on 28.08.20019.
//
import XCTest
#if OPENCOMBINE_COMPATIBILITY_TEST
import Combine
#else
import OpenCombine
#endif
@available(macOS 10.15, iOS 13.0, *)
final class ZipTests: XCTestCase {
static let arities = (2...4)
struct ChildInfo {
let subscription: CustomSubscription
let publisher: CustomPublisher
}
func testSendsExpectedValues() {
ZipTests.arities.forEach { arity in
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
(0..<arity).forEach { XCTAssertEqual(children[$0].publisher.send(1), .none) }
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(arity)])
}
}
func testChildDemand() {
[Subscribers.Demand.unlimited, .max(1)].forEach { initialDemand in
let (children, zip) = getChildrenAndZipForArity(2)
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
receiveSubscription: { downstreamSubscription = $0 })
zip.subscribe(downstreamSubscriber)
// Confirm initial demand
downstreamSubscription?.request(initialDemand)
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand)])
}
// Confirm no incremental demand
(0..<2).forEach { XCTAssertEqual(children[$0].publisher.send(1), .max(0)) }
// Confirm no additional subscription demand
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand)])
}
// Confirm value was sent
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
// Confirm subsequent demand
downstreamSubscription?.request(.max(2))
(0..<2).forEach { XCTAssertEqual(children[$0].subscription.history,
[.requested(initialDemand),
.requested(.max(2))])
}
}
}
func testDownstreamDemandRequestedWhileSendingValue() {
[Subscribers.Demand.unlimited, .max(10)].forEach { initialDemand in
let (children, zip) = getChildrenAndZipForArity(2)
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: {
downstreamSubscription = $0
$0.request(initialDemand)
},
receiveValue: { _ in
downstreamSubscription?.request(.max(666))
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
// Apple will use the result of .receive(_ input:) INSTEAD of sending
// .request to the subscription if a request is received WHILE processing
// the .receive.
// AppleRef: 001
XCTAssertEqual(children[1].publisher.send(1), .max(666))
XCTAssertEqual(children[0].subscription.history,
[.requested(initialDemand),
.requested(.max(666))])
XCTAssertEqual(children[1].subscription.history,
[.requested(initialDemand)])
}
}
func testUpstreamValueReceivedWhileSendingValue() {
let (children, zip) = getChildrenAndZipForArity(2)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in
XCTAssertEqual(children[0].publisher.send(1), .none)
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[1].publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
}
func testUpstreamFinishReceivedWhileSendingValue() {
let (children, zip) = getChildrenAndZipForArity(2)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) },
receiveValue: { _ in
children[0].publisher.send(completion: .finished)
return Subscribers.Demand.none
}
)
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[0].publisher.send(1), .none)
XCTAssertEqual(children[1].publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(2)])
}
// NOTE about how/when Apple sends .finished on `Zip`.
//
// The documentation says:
// If either upstream publisher finishes successfuly or fails with an error,
// the zipped publisher does the same.
//
// This may be true for`.failed`, but it just isn't true for `.finished`.
// The combination of tests in here confirm Apple's actual behavior. An assesment
// is made to determine if it is possible for the `Zip` to send any more values
// downstream. Roughly speaking, if any children required to provide component
// values for the next value have finished, then it will be impossible for `Zip`
// to send any more values.
// This assessment is slightly complicated by the fact that `Zip` buffers _surplus_
// component values while waiting to complete the entire tuple.
//
// AppleRef: 002 The algorithm is currently further complicated by the fact that this
// assessment is not made continuously, but rather only when one of the child
// subcriptions sends a `.finished`. This means that Apple's behavior is inconsistent.
// Sometimes, the `Zip` remains alive even though no futher emissions are possible.
// Sometimes it finishes. Ugh.
//
// If I were in charge, `Zip` would finish as soon as it becomes impossible for it
// to send another value - regarless of what triggers that change in state.
func testZipCompletesOnlyAfterAllChildrenComplete() {
let upstreamSubscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: upstreamSubscription)
let child2Publisher = CustomPublisher(subscription: upstreamSubscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriberBase<Int, TestingError>(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(child1Publisher.send(100), .none)
XCTAssertEqual(child1Publisher.send(200), .none)
XCTAssertEqual(child1Publisher.send(300), .none)
XCTAssertEqual(child2Publisher.send(1), .none)
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101)])
XCTAssertEqual(child2Publisher.send(2), .none)
XCTAssertEqual(child2Publisher.send(3), .none)
// This is so bogus. So, even though no further values are possible, Apple delays
// the completion. It seems to consider the fact that no more values are possible
// ONLY after one child sends a .finished
// Ref: 53EB
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303),
.completion(.finished)])
XCTAssertEqual(
upstreamSubscription.history,
[.requested(.unlimited), .requested(.unlimited), .cancelled]
)
}
func testUpstreamExceedsDemand() {
// Must use CustomPublisher if we want to force send a value beyond the demand
let child1Subscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: child1Subscription)
let child2Subscription = CustomSubscription()
let child2Publisher = CustomPublisher(subscription: child2Subscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
var downstreamSubscription: Subscription?
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
downstreamSubscription = $0
$0.request(.max(1))
})
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(child1Publisher.send(100), .none)
XCTAssertEqual(child2Publisher.send(1), .none)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101)])
XCTAssertEqual(child1Publisher.send(200), .none)
XCTAssertEqual(child1Publisher.send(300), .none)
XCTAssertEqual(child2Publisher.send(2), .none)
// Surplus is sent downstream despite demand of zero
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202)])
XCTAssertEqual(child2Publisher.send(3), .none)
downstreamSubscription?.request(.max(1))
// Surplus is buffered for sending when demand resumes
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.value(202),
.value(303)])
}
private func getChildrenAndZipForArity(_ childCount: Int)
-> ([ChildInfo], AnyPublisher<Int, TestingError>)
{
var children = [ChildInfo]()
for _ in (0..<childCount) {
let subscription = CustomSubscription()
let publisher = CustomPublisher(subscription: subscription)
children.append(ChildInfo(subscription: subscription,
publisher: publisher))
}
let zip: AnyPublisher<Int, TestingError>
switch childCount {
case 2:
zip = AnyPublisher(children[0].publisher.zip(children[1].publisher)
{ $0 + $1 })
case 3:
zip = AnyPublisher(children[0].publisher
.zip(children[1].publisher,
children[2].publisher) { $0 + $1 + $2 })
case 4:
zip = AnyPublisher(children[0].publisher
.zip(children[1].publisher,
children[2].publisher,
children[3].publisher) { $0 + $1 + $2 + $3 })
default:
fatalError()
}
return (children, zip)
}
func testImmediateFinishWhenOneChildFinishesWithNoSurplus() {
ZipTests.arities.forEach { arity in
for childToFinish in (0..<arity) {
let description = "Zip\(arity) childToFinish=\(childToFinish)"
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
children[childToFinish].publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)],
description)
for child in (0..<arity) {
if child == childToFinish {
XCTAssertEqual(children[child].subscription.history,
[.requested(.unlimited)],
description)
} else {
XCTAssertEqual(children[child].subscription.history,
[.requested(.unlimited),
.cancelled],
description)
}
}
}
}
}
// NOTE: This behavior betrays Apple's comments which say:
// If either upstream publisher finishes successfuly or fails with an error,
// the zipped publisher does the same.
// That appears to not be true for finishing successfully if the completing child
// has a surplus. Rather, the zip remains alive until it is impossible to deliver
// another result.
func testDelayedFinishWhenOneChildFinishesWithSurplus() {
ZipTests.arities.forEach { arity in
for childToSend in (0..<arity) {
for childToFinish in (0..<arity) {
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
_ = children[childToSend].publisher.send(666)
children[childToFinish].publisher.send(completion: .finished)
if childToSend == childToFinish {
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip")])
// Finish the others
(0..<arity)
.filter { $0 != childToFinish }
.forEach( {
children[$0].publisher.send(completion: .finished)
})
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)])
} else {
XCTAssertEqual(downstreamSubscriber.history,
[.subscription("Zip"),
.completion(.finished)])
}
}
}
}
}
func testBCancelledAfterAFailed() {
let child1Subscription = CustomSubscription()
let child1Publisher = CustomPublisher(subscription: child1Subscription)
let child2Subscription = CustomSubscription()
let child2Publisher = CustomPublisher(subscription: child2Subscription)
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(receiveSubscription: {
$0.request(.unlimited)
})
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
XCTAssertEqual(child1Subscription.history, [.requested(.unlimited),
.cancelled])
XCTAssertEqual(child2Subscription.history, [.requested(.unlimited),
.cancelled])
}
func testAValueAfterAChildFinishedWithoutSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
// This is strange and inconsistent. In other cases, zip doesn't complete
// until ALL children have completed
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child1Publisher.send(200)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testBValueAfterAChildFinishedWithoutSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
// This is strange and inconsistent. In other cases, zip doesn't complete
// until ALL children have completed
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testAValueAfterAChildFinishedWithSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(200)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
}
func testBValueAfterAChildFinishedWithSurplus() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip")])
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
child2Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(101),
.completion(.finished)])
}
func testValueAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(100)
child1Publisher.send(completion: .failure(.oops))
child2Publisher.send(1)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testFinishAfterFinished() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .finished)
child2Publisher.send(completion: .finished)
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testFinishAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
child1Publisher.send(completion: .finished)
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testFailedAfterFinished() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .finished)
child2Publisher.send(completion: .finished)
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.finished)])
}
func testFailedAfterFailed() {
let child1Publisher = PassthroughSubject<Int, TestingError>()
let child2Publisher = PassthroughSubject<Int, TestingError>()
let zip = child1Publisher.zip(child2Publisher) { $0 + $1 }
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
child1Publisher.send(completion: .failure(.oops))
child1Publisher.send(completion: .failure(.oops))
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.completion(.failure(.oops))])
}
func testZip2Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher) })
}
func testZip3Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
let child3Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 42,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher, child3Publisher) })
}
func testZip4Lifecycle() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
let child3Publisher = PassthroughSubject<Int, TestingError>()
let child4Publisher = PassthroughSubject<Int, TestingError>()
try testLifecycle(sendValue: 31,
cancellingSubscriptionReleasesSubscriber: false,
{ $0.zip(child2Publisher, child3Publisher, child4Publisher) })
}
func testZipReceiveSubscriptionTwice() throws {
let child2Publisher = PassthroughSubject<Int, TestingError>()
// Can't use `testReceiveSubscriptionTwice` helper here as `(Int, Int)` output
// can't be made `Equatable`.
let helper = OperatorTestHelper(
publisherType: CustomPublisher.self,
initialDemand: nil,
receiveValueDemand: .none,
createSut: { $0.zip(child2Publisher) }
)
XCTAssertEqual(helper.subscription.history, [])
let secondSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: secondSubscription)
XCTAssertEqual(secondSubscription.history, [.cancelled])
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: helper.subscription)
XCTAssertEqual(helper.subscription.history, [.cancelled])
try XCTUnwrap(helper.downstreamSubscription).cancel()
XCTAssertEqual(helper.subscription.history, [.cancelled, .cancelled])
let thirdSubscription = CustomSubscription()
try XCTUnwrap(helper.publisher.subscriber)
.receive(subscription: thirdSubscription)
}
func testNoDemandOnSubscriptionCrashes() {
ZipTests.arities.forEach { arity in
let (_, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(
receiveSubscription: { subscription in
self.assertCrashes { subscription.request(.none) }
}
)
zip.subscribe(downstreamSubscriber)
}
}
func testIncreasedDemand() throws {
ZipTests.arities.forEach { arity in
let (children, zip) = getChildrenAndZipForArity(arity)
let downstreamSubscriber = TrackingSubscriber(
receiveValue: { _ in
.max(1)
}
)
zip.subscribe(downstreamSubscriber)
(0..<arity).forEach {
let demand = children[$0].publisher.send(1)
if $0 == arity - 1 {
XCTAssertEqual(demand, .max(1))
} else {
XCTAssertEqual(demand, .none)
}
}
XCTAssertEqual(downstreamSubscriber.history, [.subscription("Zip"),
.value(arity)])
}
}
func testZipCurrentValueSubject() throws {
let subject = CurrentValueSubject<Void, Never>(())
let zip = [42].publisher.zip(subject)
let downstreamSubscriber = TrackingSubscriberBase<(Int, ()), Never>(
receiveSubscription: { $0.request(.unlimited) })
zip.subscribe(downstreamSubscriber)
let history = downstreamSubscriber.history
XCTAssertEqual(history.count, 3)
// tuples aren't Equatable, so matching the elements one by one
switch history[0] {
case .subscription("Zip"):
break
default:
XCTFail("Failed to match the first subscription event in \(#function)")
}
switch history[1] {
case .value(let v):
if v.0 != 42 || v.1 != () {
XCTFail("Failed to match the value event in \(#function)")
}
default:
XCTFail("Failed to match the value event in \(#function)")
}
switch history[2] {
case .completion(.finished):
break
default:
XCTFail("Failed to match the completion event in \(#function)")
}
}
}
@@ -137,4 +137,41 @@ final class AssignTests: XCTestCase {
publisher.send(100)
XCTAssertEqual(object.value, 42)
}
func testReceiveCompletionWhileCancelling() {
let cancellable: AnyCancellable
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCompleting() {
let cancellable: AnyCancellable
let finish: () -> Void
do {
let object = ObjectToModify()
cancellable = object.autofinish.assign(to: \.value, on: object)
let underlyingPublisher = object.autofinish.publisher
finish = { underlyingPublisher.send(completion: .finished) }
}
finish() // autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
}
@available(macOS 10.15, iOS 13.0, *)
final class ObjectToModify {
let autofinish = AutomaticallyFinish<Int, Never>()
var value = 0
}
@@ -210,6 +210,82 @@ final class SinkTests: XCTestCase {
releasesClosures: true)
}
func testReceiveCompletionWhileCancelling() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in },
receiveValue: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
}
)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCancelling2() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
},
receiveValue: { _ in }
)
}
// autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
}
func testReceiveCompletionWhileCompleting() {
// https://github.com/OpenCombine/OpenCombine/issues/208
let cancellable: AnyCancellable
let finish: () -> Void
var receiveCompletionCalled = false
do {
let autofinish = AutomaticallyFinish<Int, Never>()
cancellable = autofinish.listen(
receiveCompletion: { _ in
receiveCompletionCalled = true
},
receiveValue: { _ in
XCTFail("Should not be called")
_ = autofinish // capture
}
)
let underlyingPublisher = autofinish.publisher
finish = { underlyingPublisher.send(completion: .finished) }
}
finish() // autofinish is deallocated here, a completion is sent to the sink
cancellable.cancel()
XCTAssertTrue(receiveCompletionCalled)
}
func testRecursiveCompletion() {
var recursionCounter = 10
var delayedSink: Sut?
+19 -7
View File
@@ -15,16 +15,25 @@ import re
from pathlib import Path
from argparse import ArgumentParser
class Test:
def __init__(self, name, is_async):
self.name = name
self.is_async = is_async
def __str__(self):
return self.name
TEST_METHOD_PATTERN = \
re.compile(r"func\s*(test\w+)\s*\(\s*\)\s*(?:throws\s*)?{")
re.compile(r"func\s*(test\w+)\s*\(\s*\)\s*(async\s*)?(?:throws\s*)?{")
TEST_DISCOVERY_CONDITION_PATTERN = \
re.compile(r"^#if (.+)\s*\/\/\s*TEST_DISCOVERY_CONDITION\s*$", flags=re.MULTILINE)
def extract_test_names(test_file):
contents = test_file.read_text()
test_names = [match[1] for match in TEST_METHOD_PATTERN.finditer(contents)]
tests = [Test(match[1], match[2] is not None) for match in TEST_METHOD_PATTERN.finditer(contents)]
condition = TEST_DISCOVERY_CONDITION_PATTERN.search(contents)
return (test_names, condition[1] if condition else None)
return (tests, condition[1] if condition else None)
def generate_linuxmain(workdir):
workdir = Path(workdir)
@@ -41,14 +50,17 @@ var tests = [XCTestCaseEntry]()
""")
for test_file in test_files:
(test_names, condition) = extract_test_names(test_file)
if not test_names:
(tests, condition) = extract_test_names(test_file)
if not tests:
continue
if condition:
linuxmain.write(f"#if {condition}\n")
linuxmain.write(f"let allTests_{test_file.stem} = [\n")
for test_name in test_names:
linuxmain.write(f" (\"{test_name}\", {test_file.stem}.{test_name}),\n")
for test in tests:
if test.is_async:
linuxmain.write(f" (\"{test}\", asyncTest({test_file.stem}.{test})),\n")
else:
linuxmain.write(f" (\"{test}\", {test_file.stem}.{test}),\n")
linuxmain.write("]\n")
linuxmain.write(f"tests.append(testCase(allTests_{test_file.stem}))\n")
if condition: