Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cf67e3637 | |||
| 3877609ba2 | |||
| 734e7e39cb | |||
| 2085bb7593 | |||
| 5b247a5a01 | |||
| 64f436c748 | |||
| b4e6313814 | |||
| a0cf895c8c | |||
| dec7d4a569 | |||
| fdc7550ff7 | |||
| 135dc9a8ab | |||
| 37a4fe400f | |||
| 7b466153a6 | |||
| baac42a0ad | |||
| fd05f5c8ff | |||
| 8bfdcd4295 | |||
| 90454807b4 | |||
| 77374fa820 | |||
| 8eda9d7e3d | |||
| 35cfe51c72 | |||
| 42c0fa02ae | |||
| 999a29cdf9 | |||
| 36edf4819b | |||
| dfac8a9da7 | |||
| 070ed94d18 | |||
| ea8938db72 | |||
| 4392b4610c | |||
| c96f2e300d | |||
| 94de7bae46 | |||
| ed1b06ba51 | |||
| 4b2c87a0bb | |||
| 0243fd063d | |||
| 4fed5e9a5a | |||
| 80a4915715 | |||
| 4716805f12 | |||
| 5490ff9be9 | |||
| c911862a24 | |||
| f823f7b18c | |||
| 02d1494ce9 | |||
| f69bf6af64 | |||
| 866d837cdf | |||
| 7d0a8cd6f8 | |||
| dfd3cdf890 | |||
| ef0288e075 | |||
| f219d6f6a5 | |||
| 710dfa2715 | |||
| 791625ff3b | |||
| 7e4cdde419 | |||
| 096e245d02 | |||
| 1879860f35 | |||
| ace5778817 | |||
| 12700a0500 | |||
| 6c8108f9dc | |||
| b27b2c31ce | |||
| 3d3adb564b | |||
| 925bee4af9 | |||
| adcee8c14d | |||
| 29126ac259 | |||
| bab8e08d2f | |||
| 4060ee9f57 | |||
| 5996772433 | |||
| cd45c77fac | |||
| e618d179fe | |||
| 4fa5f48c19 |
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,6 +2,8 @@ included:
|
||||
- Sources
|
||||
- Tests
|
||||
|
||||
child_config: Tests/.swiftlint.yml
|
||||
|
||||
disabled_rules:
|
||||
- block_based_kvo
|
||||
- class_delegate_protocol
|
||||
|
||||
+3
-3
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
# OpenCombine
|
||||
[](https://circleci.com/gh/OpenCombine/OpenCombine)
|
||||
[](https://codecov.io/gh/OpenCombine/OpenCombine)
|
||||

|
||||

|
||||

|
||||
[<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** |
|
||||
|---|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/compatibility_tests.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/macos.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/ubuntu.yml)|
|
||||
|[](https://github.com/OpenCombine/OpenCombine/actions/workflows/windows.yml)|
|
||||
|[](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
@@ -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 won’t 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 won’t 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 won’t 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 won’t 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 won’t 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 won’t 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"
|
||||
|
||||
@@ -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 publisher’s 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
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:)`.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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 aren’t 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 won’t 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 won’t 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 won’t 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 won’t 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 won’t 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 won’t 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 doesn’t 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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user