Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46007658a1 | |||
| c4c7f2172d | |||
| 5b0a21a0b9 | |||
| f4e191b2ff | |||
| c275e51cdc | |||
| a84105133c | |||
| ef3ebd965a | |||
| a08b99c886 | |||
| 3398499540 | |||
| 1bf193ddaa | |||
| 3a88dfd76b | |||
| bd0b69d7cb | |||
| dba76c3c41 | |||
| 5863492753 | |||
| 2f2e16ee1f | |||
| bca131c2a4 | |||
| e999fafdce | |||
| b38830e0f1 | |||
| 525405f64d | |||
| 693d1145f8 | |||
| 2f9ddc2229 | |||
| bcd1b727f8 | |||
| 5d1034fcc0 | |||
| 2378f3d97e | |||
| 4a965830e7 | |||
| 9eabadb7c9 | |||
| dcfaec2c9d | |||
| 219ee38119 | |||
| 3a5389d398 | |||
| 69ead1c8fb | |||
| 8e6404592e | |||
| 14b7ced2fe | |||
| d7b9e87f6d | |||
| 4fd04b8a00 |
@@ -0,0 +1,3 @@
|
||||
*.swift.gyb linguist-language=Swift
|
||||
|
||||
**/GENERATED-* linguist-generated=true
|
||||
+110
@@ -43,3 +43,113 @@ DerivedData/
|
||||
# End of https://www.gitignore.io/api/Xcode
|
||||
|
||||
.idea
|
||||
|
||||
# Created by https://www.gitignore.io/api/Python
|
||||
# Edit at https://www.gitignore.io/?templates=Python
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# Mr Developer
|
||||
.mr.developer.cfg
|
||||
.project
|
||||
.pydevproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# End of https://www.gitignore.io/api/Python
|
||||
|
||||
+7
-11
@@ -18,19 +18,19 @@ cache:
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- name: "Ubuntu 16.04 | Swift 5.1 | Tests"
|
||||
- name: "Ubuntu 16.04 | Swift 5.1.1 | Tests"
|
||||
os: linux
|
||||
dist: xenial
|
||||
sudo: required
|
||||
env: SWIFT_VERSION="swift-5.1-DEVELOPMENT-SNAPSHOT-2019-09-05-a" OPENCOMBINE_TEST="YES"
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Tests"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
env: SWIFT_VERSION="5.0" CODE_COVERAGE="YES" OPENCOMBINE_TEST="YES"
|
||||
# - name: "iOS 13.0 | Swift 5.1 | Compatibility Tests"
|
||||
# os: osx
|
||||
# osx_image: xcode11
|
||||
# env: SWIFT_VERSION="5.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "iOS 13.1 | Swift 5.1.1 | Compatibility Tests"
|
||||
os: osx
|
||||
osx_image: xcode11.1
|
||||
env: SWIFT_VERSION="5.1.1" OPENCOMBINE_COMPATIBILITY_TEST="YES"
|
||||
- name: "macOS 10.14 | Swift 5.0 | Code Quality"
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
@@ -74,11 +74,7 @@ script:
|
||||
fi
|
||||
- if [[ $OPENCOMBINE_COMPATIBILITY_TEST == "YES" ]]; then
|
||||
make generate-compatibility-xcodeproj;
|
||||
set -o pipefail && xcodebuild \
|
||||
-scheme OpenCombine-Package \
|
||||
-sdk iphonesimulator13.0 \
|
||||
-destination "platform=iOS Simulator,name=iPhone Xs,OS=13.0" \
|
||||
build test | xcpretty;
|
||||
set -o pipefail && xcodebuild -scheme OpenCombine-Package -sdk iphonesimulator13.1 -destination "platform=iOS Simulator,name=iPhone 11,OS=13.1" build test | xcpretty;
|
||||
fi
|
||||
- if [[ $SWIFT_LINT == "YES" ]]; then
|
||||
swiftlint lint --strict --reporter "emoji";
|
||||
|
||||
@@ -1,7 +1,71 @@
|
||||
import Danger
|
||||
import Foundation
|
||||
|
||||
extension StringProtocol {
|
||||
func dropSuffix<S: StringProtocol>(_ suffix: S) -> SubSequence {
|
||||
if hasSuffix(suffix) {
|
||||
return self[..<index(endIndex, offsetBy: -suffix.count)]
|
||||
} else {
|
||||
return self[...]
|
||||
}
|
||||
}
|
||||
|
||||
func directoryAndFileName() -> (SubSequence, SubSequence) {
|
||||
let lastPathSeparator = lastIndex(of: "/")
|
||||
if let lastPathSeparator = lastPathSeparator {
|
||||
return (self[..<lastPathSeparator], self[index(after: lastPathSeparator)...])
|
||||
} else {
|
||||
return (".", self[...])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let danger = Danger()
|
||||
|
||||
let allCreatedAndModified = danger.git.createdFiles + danger.git.modifiedFiles
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a GYB template but forgot to run `make gyb`.
|
||||
|
||||
let modifiedTemplates = allCreatedAndModified.filter { $0.hasSuffix(".gyb") }
|
||||
|
||||
for modifiedTemplate in modifiedTemplates {
|
||||
let (directory, filename) = modifiedTemplate.directoryAndFileName()
|
||||
let generated = "\(directory)/GENERATED-\(filename.dropSuffix(".gyb"))"
|
||||
|
||||
if !allCreatedAndModified.contains(generated) {
|
||||
fail("""
|
||||
A template \(modifiedTemplate) was modified, but the file \(generated) \
|
||||
was not regenerated.
|
||||
|
||||
Run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
// Fail if the committer modified a generated file.
|
||||
// A template should be modified instead.
|
||||
|
||||
for modifiedGeneratedFile in danger.git.modifiedFiles
|
||||
where modifiedGeneratedFile.contains("GENERATED-")
|
||||
{
|
||||
let template = modifiedGeneratedFile
|
||||
.replacingOccurrences(of: "GENERATED-", with: "") + ".gyb"
|
||||
|
||||
if !danger.git.modifiedFiles.contains(template) {
|
||||
fail("""
|
||||
A generated file \(modifiedGeneratedFile) was modified, but \
|
||||
the template it was generated from was not modified.
|
||||
|
||||
Please modify the template \(template) instead, \
|
||||
run `make gyb` from the root of the project and commit the changes.
|
||||
""")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SwiftLint.lint(inline: true,
|
||||
configFile: ".swiftlint.yml",
|
||||
strict: true,
|
||||
|
||||
@@ -10,7 +10,7 @@ test-debug:
|
||||
swift test -c debug $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-debug-sanitize-thread:
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
swift test -c debug --sanitize thread $(SWIFT_TEST_FLAGS)
|
||||
|
||||
test-release:
|
||||
swift test -c release $(SWIFT_TEST_FLAGS)
|
||||
@@ -28,6 +28,9 @@ generate-compatibility-xcodeproj:
|
||||
generate-xcodeproj:
|
||||
swift package generate-xcodeproj --enable-code-coverage
|
||||
|
||||
gyb:
|
||||
$(shell ./utils/recursively_gyb.sh)
|
||||
|
||||
clean:
|
||||
rm -rf .build
|
||||
|
||||
@@ -38,4 +41,5 @@ clean:
|
||||
test-compatibility-debug \
|
||||
generate-compatibility-xcodeproj \
|
||||
generate-xcodeproj \
|
||||
gyb \
|
||||
clean
|
||||
|
||||
@@ -22,9 +22,30 @@ You can refer to [this gist](https://gist.github.com/broadwaylamb/c2c8550d76b3ff
|
||||
You can run compatibility tests against Apple's Combine. In order to do that you will need either macOS 10.14 with iOS 13 simulator installed (since the only way we can get Apple's Combine on macOS 10.14 is using the simulator), or macOS 10.15 (Apple's Combine is bundled with the OS). Execute the following command from the root of the package:
|
||||
|
||||
```
|
||||
$ swift test -Xswiftc -DOPENCOMBINE_COMPATIBILITY_TEST
|
||||
$ make test-compatibility
|
||||
```
|
||||
|
||||
Or enable the `-DOPENCOMBINE_COMPATIBILITY_TEST` compiler flag in Xcode's build settings. Note that on iOS only the latter will work.
|
||||
|
||||
> NOTE: Before starting to work on some feature, please consult the [GitHub project](https://github.com/broadwaylamb/OpenCombine/projects/2) to make sure that nobody's already making progress on the same feature! If not, then please create a draft PR to indicate that you're beginning your work.
|
||||
|
||||
#### GYB
|
||||
|
||||
Some publishers in OpenCombine (like `Publishers.MapKeyPath`, `Publishers.Merge`) exist in several
|
||||
different flavors in order to support several arities. For example, there are also `Publishers.MapKeyPath2`
|
||||
and `Publishers.MapKeyPath3`, which are very similar but different enough that Swift's type system
|
||||
can't help us here (because there's no support for variadic generics). Maintaining multiple instances of
|
||||
those generic types is tedious and error-prone (they can get out of sync), so we use the GYB tool for
|
||||
generating those instances from a template.
|
||||
|
||||
GYB is a Python script that evaluates Python code written inside a template file, so it's very flexible —
|
||||
templates can be arbitrarily complex. There is a good article about GYB on
|
||||
[NSHipster](https://nshipster.com/swift-gyb/).
|
||||
|
||||
GYB is part of the [Swift Open Source Project](https://github.com/apple/swift/blob/master/utils/gyb.py)
|
||||
and can be distributed under the same license as Swift itself.
|
||||
|
||||
GYB template files have the `.gyb` extension. Run `make gyb` to generate Swift code from those
|
||||
templates. The generated files are prefixed with `GENERATED-` and are checked into source control. Those
|
||||
files should never be edited directly. Instead, the `.gyb` template should be edited, and after that the files
|
||||
should be regenerated using `make gyb`.
|
||||
|
||||
+44
-824
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,127 @@
|
||||
#include "COpenCombineHelpers.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <cstdlib>
|
||||
|
||||
extern "C" uint64_t opencombine_next_combine_identifier() {
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
#ifdef __APPLE__
|
||||
#include <os/lock.h>
|
||||
#endif // __APPLE__
|
||||
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_BEGIN try {
|
||||
|
||||
#define OPENCOMBINE_HANDLE_EXCEPTION_END } catch (...) { abort(); }
|
||||
|
||||
namespace {
|
||||
|
||||
static std::atomic<uint64_t> next_combine_identifier;
|
||||
|
||||
class PlatformIndependentMutex {
|
||||
public:
|
||||
virtual void lock() = 0;
|
||||
virtual void unlock() = 0;
|
||||
|
||||
virtual ~PlatformIndependentMutex() {}
|
||||
};
|
||||
|
||||
template <typename Mutex>
|
||||
class GenericMutex final : PlatformIndependentMutex {
|
||||
Mutex mutex_;
|
||||
public:
|
||||
void lock() override {
|
||||
mutex_.lock();
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
mutex_.unlock();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef __APPLE__
|
||||
bool isOSUnfairLockAvailable() {
|
||||
// We're linking weakly, so if we're back-deploying, this will be null.
|
||||
return os_unfair_lock_lock != nullptr;
|
||||
}
|
||||
|
||||
template <>
|
||||
class GenericMutex<os_unfair_lock> final : PlatformIndependentMutex {
|
||||
os_unfair_lock mutex_ = OS_UNFAIR_LOCK_INIT;
|
||||
public:
|
||||
GenericMutex() = default;
|
||||
GenericMutex(const GenericMutex&) = delete;
|
||||
GenericMutex& operator=(const GenericMutex&) = delete;
|
||||
|
||||
void lock() override {
|
||||
os_unfair_lock_lock(&mutex_);
|
||||
}
|
||||
|
||||
void unlock() override {
|
||||
os_unfair_lock_unlock(&mutex_);
|
||||
}
|
||||
};
|
||||
#endif // __APPLE__
|
||||
|
||||
} // end anonymous namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void) {
|
||||
return next_combine_identifier.fetch_add(1);
|
||||
}
|
||||
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (isOSUnfairLockAvailable()) {
|
||||
return {new GenericMutex<os_unfair_lock>};
|
||||
} else {
|
||||
return {new GenericMutex<std::mutex>};
|
||||
}
|
||||
#else
|
||||
return {new GenericMutex<std::mutex>};
|
||||
#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 GenericMutex<std::recursive_mutex>};
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock lock) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock lock) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(lock.opaque)->lock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock mutex) {
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_BEGIN
|
||||
static_cast<PlatformIndependentMutex*>(mutex.opaque)->unlock();
|
||||
OPENCOMBINE_HANDLE_EXCEPTION_END
|
||||
}
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock) {
|
||||
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
|
||||
}
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock) {
|
||||
return delete static_cast<PlatformIndependentMutex*>(lock.opaque);
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
|
||||
@@ -10,11 +10,60 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#if __has_attribute(swift_name)
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name) __attribute__((swift_name(#_name)))
|
||||
#else
|
||||
# define OPENCOMBINE_SWIFT_NAME(_name)
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
uint64_t opencombine_next_combine_identifier();
|
||||
#pragma mark - CombineIdentifier
|
||||
|
||||
uint64_t opencombine_next_combine_identifier(void)
|
||||
OPENCOMBINE_SWIFT_NAME(nextCombineIdentifier());
|
||||
|
||||
#pragma mark - OpenCombineUnfairLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairLock) OpenCombineUnfairLock;
|
||||
|
||||
/// Allocates a lock object. The allocated object must be destroyed by calling
|
||||
/// the destroy() method.
|
||||
OpenCombineUnfairLock opencombine_unfair_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.allocate());
|
||||
|
||||
void opencombine_unfair_lock_lock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_lock_unlock(OpenCombineUnfairLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_lock_dealloc(OpenCombineUnfairLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairLock.deallocate(self:));
|
||||
|
||||
#pragma mark - OpenCombineUnfairRecursiveLock
|
||||
|
||||
/// A wrapper around an opaque pointer for type safety in Swift.
|
||||
typedef struct OpenCombineUnfairRecursiveLock {
|
||||
void* _Nonnull opaque;
|
||||
} OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock) OpenCombineUnfairRecursiveLock;
|
||||
|
||||
OpenCombineUnfairRecursiveLock opencombine_unfair_recursive_lock_alloc(void)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.allocate());
|
||||
|
||||
void opencombine_unfair_recursive_lock_lock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.lock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_unlock(OpenCombineUnfairRecursiveLock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.unlock(self:));
|
||||
|
||||
void opencombine_unfair_recursive_lock_dealloc(OpenCombineUnfairRecursiveLock lock)
|
||||
OPENCOMBINE_SWIFT_NAME(UnfairRecursiveLock.deallocate(self:));
|
||||
|
||||
#ifdef __cplusplus
|
||||
} // extern "C"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
/// Subscriber implementations can use this type to provide a “cancellation token” that
|
||||
/// makes it possible for a caller to cancel a publisher, but not to use the
|
||||
/// `Subscription` object to request items.
|
||||
/// An AnyCancellable instance automatically calls `cancel()` when deinitialized.
|
||||
public final class AnyCancellable: Cancellable, Hashable {
|
||||
|
||||
private var _cancel: (() -> Void)?
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Wraps this publisher with a type eraser.
|
||||
///
|
||||
/// Use `eraseToAnyPublisher()` to expose an instance of `AnyPublisher` to
|
||||
/// the downstream subscriber, rather than this publisher’s actual type.
|
||||
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
|
||||
return .init(self)
|
||||
}
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
// Created by Sergej Jaskiewicz on 10.06.2019.
|
||||
//
|
||||
|
||||
import func COpenCombineHelpers.opencombine_next_combine_identifier
|
||||
import func COpenCombineHelpers.nextCombineIdentifier
|
||||
|
||||
public struct CombineIdentifier: Hashable, CustomStringConvertible {
|
||||
|
||||
private let id: UInt64
|
||||
|
||||
public init() {
|
||||
self.id = opencombine_next_combine_identifier()
|
||||
self.id = nextCombineIdentifier()
|
||||
}
|
||||
|
||||
public init(_ obj: AnyObject) {
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that wraps a single value and publishes a new element whenever the value
|
||||
/// changes.
|
||||
public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = unfairRecursiveLock()
|
||||
private let _lock = UnfairRecursiveLock.allocate()
|
||||
|
||||
// TODO: Combine uses bag data structure
|
||||
private var _subscriptions: [Conduit] = []
|
||||
@@ -43,6 +45,7 @@ public final class CurrentValueSubject<Output, Failure: Error>: Subject {
|
||||
for subscription in _subscriptions {
|
||||
subscription._downstream = nil
|
||||
}
|
||||
_lock.deallocate()
|
||||
}
|
||||
|
||||
public func send(subscription: Subscription) {
|
||||
|
||||
@@ -5,19 +5,7 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#else
|
||||
#error("How to do locking on this platform?")
|
||||
#endif
|
||||
|
||||
@usableFromInline
|
||||
internal protocol UnfairLock: AnyObject {
|
||||
func lock()
|
||||
func unlock()
|
||||
}
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension UnfairLock {
|
||||
|
||||
@@ -29,118 +17,12 @@ extension UnfairLock {
|
||||
}
|
||||
}
|
||||
|
||||
internal protocol UnfairRecursiveLock: UnfairLock {}
|
||||
extension UnfairRecursiveLock {
|
||||
|
||||
internal func unfairLock() -> UnfairLock {
|
||||
#if canImport(Darwin)
|
||||
if #available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *) {
|
||||
return OSUnfairLock()
|
||||
} else {
|
||||
return PThreadMutexLock()
|
||||
}
|
||||
#else
|
||||
return PThreadMutexLock()
|
||||
#endif
|
||||
}
|
||||
|
||||
internal func unfairRecursiveLock() -> UnfairRecursiveLock {
|
||||
// TODO: Use os_unfair_recursive_lock on Darwin as soon as it becomes public API.
|
||||
return PThreadMutexRecursiveLock()
|
||||
}
|
||||
|
||||
private class PThreadMutexLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private let mutex = UnsafeMutablePointer<pthread_mutex_t>.allocate(capacity: 1)
|
||||
|
||||
init() {
|
||||
var status: CInt
|
||||
var attributes = pthread_mutexattr_t()
|
||||
status = pthread_mutexattr_init(&attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_init returned non-zero status: \(status)")
|
||||
|
||||
// Enable error detection
|
||||
status = pthread_mutexattr_settype(&attributes, CInt(PTHREAD_MUTEX_ERRORCHECK))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
|
||||
setAdditionalAttributes(&attributes)
|
||||
|
||||
status = pthread_mutex_init(mutex, &attributes)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_init returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
fileprivate func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
// Do nothing for non-recursive locks
|
||||
}
|
||||
|
||||
final func lock() {
|
||||
let status = pthread_mutex_lock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final func unlock() {
|
||||
let status = pthread_mutex_unlock(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_lock returned non-zero status: \(status)")
|
||||
}
|
||||
|
||||
final var description: String { return String(describing: mutex.pointee) }
|
||||
|
||||
final var customMirror: Mirror { return Mirror(reflecting: mutex.pointee) }
|
||||
|
||||
final var playgroundDescription: Any { return description }
|
||||
|
||||
deinit {
|
||||
let status = pthread_mutex_destroy(mutex)
|
||||
precondition(status == 0,
|
||||
"pthread_mutex_destroy returned non-zero status: \(status)")
|
||||
mutex.deallocate()
|
||||
@inlinable
|
||||
internal func `do`<Result>(_ body: () throws -> Result) rethrows -> Result {
|
||||
lock()
|
||||
defer { unlock() }
|
||||
return try body()
|
||||
}
|
||||
}
|
||||
|
||||
private final class PThreadMutexRecursiveLock: PThreadMutexLock, UnfairRecursiveLock {
|
||||
override func setAdditionalAttributes(
|
||||
_ attributes: UnsafeMutablePointer<pthread_mutexattr_t>
|
||||
) {
|
||||
let status = pthread_mutexattr_settype(attributes, CInt(PTHREAD_MUTEX_RECURSIVE))
|
||||
precondition(status == 0,
|
||||
"pthread_mutexattr_settype returned non-zero status: \(status)")
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(Darwin)
|
||||
|
||||
@available(macOS 10.12, iOS 10.0, tvOS 10.0, watchOS 3.0, *)
|
||||
private final class OSUnfairLock
|
||||
: UnfairLock,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
{
|
||||
private var mutex = os_unfair_lock()
|
||||
|
||||
func lock() {
|
||||
os_unfair_lock_lock(&mutex)
|
||||
}
|
||||
|
||||
func unlock() {
|
||||
os_unfair_lock_unlock(&mutex)
|
||||
}
|
||||
|
||||
var description: String { return String(describing: mutex) }
|
||||
|
||||
var customMirror: Mirror { return Mirror(reflecting: mutex) }
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
|
||||
#endif // canImport(Darwin)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// PartialCompletion.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
/// A value of this type is returned by the overridden `receive(newValue:)` method
|
||||
/// of the `ReduceProducer` and `FilterProducer` classes.
|
||||
internal enum PartialCompletion<Value, Failure: Error> {
|
||||
|
||||
/// Indicate that we should continue accepting the upstream's output.
|
||||
case `continue`(Value)
|
||||
|
||||
/// Indicate that no values should be received from the upstream anymore.
|
||||
case finished
|
||||
|
||||
/// Indicate that there was a failure and we should send it downstream.
|
||||
case failure(Failure)
|
||||
}
|
||||
|
||||
extension PartialCompletion where Value == Void {
|
||||
|
||||
/// Indicate that we should continue accepting the upstream's output.
|
||||
internal static var `continue`: PartialCompletion { return .continue(()) }
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
//
|
||||
// ReduceProducer.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 22.09.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A helper class that acts like both subscriber and subscription.
|
||||
///
|
||||
/// Reduce-like operators send an instance of their `Inner` class that is subclass
|
||||
/// of this class to the upstream publisher (as subscriber) and
|
||||
/// to the downstream subcriber (as subsription).
|
||||
///
|
||||
/// Reduce-like operators include `Publishers.Reduce`, `Publishers.TryReduce`,
|
||||
/// `Publishers.Count`, `Publishers.FirstWhere`, `Publishers.AllSatisfy` and more.
|
||||
///
|
||||
/// Subclasses must override the `receive(newValue:)` and `description`.
|
||||
internal class ReduceProducer<Downstream: Subscriber,
|
||||
Input,
|
||||
Output,
|
||||
UpstreamFailure: Error,
|
||||
Reducer>
|
||||
: CustomStringConvertible,
|
||||
CustomReflectable
|
||||
where Downstream.Input == Output
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety
|
||||
|
||||
// MARK: - State
|
||||
|
||||
internal final var result: Output?
|
||||
|
||||
private let initial: Output?
|
||||
|
||||
internal final let reduce: Reducer
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var downstreamRequested = false
|
||||
|
||||
private var cancelled = false
|
||||
|
||||
private var completed = false
|
||||
|
||||
private var upstreamCompleted = false
|
||||
|
||||
private var empty = true
|
||||
|
||||
internal init(downstream: Downstream, initial: Output?, reduce: Reducer) {
|
||||
self.downstream = downstream
|
||||
self.initial = initial
|
||||
self.result = initial
|
||||
self.reduce = reduce
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
// MARK: - Abstract methods
|
||||
|
||||
internal func receive(
|
||||
newValue: Input
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
internal var description: String {
|
||||
abstractMethod()
|
||||
}
|
||||
|
||||
// MARK: - CustomReflectable
|
||||
|
||||
internal var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("result", result as Any),
|
||||
("initial", initial as Any),
|
||||
("status", status)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
/// - Precondition: `lock` is held.
|
||||
private func receiveFinished() {
|
||||
guard !cancelled, !completed, !upstreamCompleted else {
|
||||
lock.unlock()
|
||||
// This should never happen, because `receive(completion:)`
|
||||
// (from which this function is called) early exists if
|
||||
// `status` is `.terminal`.
|
||||
assertionFailure("The subscription should have been terminated by now")
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
self.completed = downstreamRequested || empty
|
||||
let completed = self.completed
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
|
||||
if completed {
|
||||
sendResultAndFinish(result)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Precondition: `lock` is held.
|
||||
private func receiveFailure(_ failure: UpstreamFailure) {
|
||||
guard !cancelled, !completed, !upstreamCompleted else {
|
||||
lock.unlock()
|
||||
// This should never happen, because `receive(completion:)`
|
||||
// (from which this function is called) early exists if
|
||||
// `status` is `.terminal`.
|
||||
assertionFailure("The subscription should have been terminated by now")
|
||||
return
|
||||
}
|
||||
upstreamCompleted = true
|
||||
completed = true
|
||||
lock.unlock()
|
||||
downstream.receive(completion: .failure(failure as! Downstream.Failure))
|
||||
}
|
||||
|
||||
private func sendResultAndFinish(_ result: Output?) {
|
||||
assert(completed && upstreamCompleted)
|
||||
if let result = result {
|
||||
_ = downstream.receive(result)
|
||||
}
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
}
|
||||
|
||||
extension ReduceProducer: Subscriber {
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
internal func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
empty = false
|
||||
lock.unlock()
|
||||
|
||||
// Combine doesn't hold the lock when calling `receive(newValue:)`.
|
||||
//
|
||||
// This can lead to data races if the contract is violated
|
||||
// (like when we receive input from multiple threads simultaneously).
|
||||
switch self.receive(newValue: input) {
|
||||
case .continue:
|
||||
break
|
||||
case .finished:
|
||||
lock.lock()
|
||||
upstreamCompleted = true
|
||||
let downstreamRequested = self.downstreamRequested
|
||||
if downstreamRequested {
|
||||
completed = true
|
||||
}
|
||||
status = .terminal
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
|
||||
subscription.cancel()
|
||||
|
||||
guard downstreamRequested else { break }
|
||||
|
||||
sendResultAndFinish(result)
|
||||
case let .failure(error):
|
||||
lock.lock()
|
||||
upstreamCompleted = true
|
||||
completed = true
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
}
|
||||
|
||||
return .none
|
||||
}
|
||||
|
||||
internal func receive(completion: Subscribers.Completion<UpstreamFailure>) {
|
||||
lock.lock()
|
||||
guard case .subscribed = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
switch completion {
|
||||
case .finished:
|
||||
receiveFinished()
|
||||
case let .failure(error):
|
||||
receiveFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReduceProducer: Subscription {
|
||||
internal func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard !downstreamRequested, !cancelled, !completed else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
downstreamRequested = true
|
||||
guard upstreamCompleted else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
completed = true
|
||||
let result = self.result
|
||||
lock.unlock()
|
||||
sendResultAndFinish(result)
|
||||
}
|
||||
|
||||
internal func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
cancelled = true
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension ReduceProducer: CustomPlaygroundDisplayConvertible {
|
||||
internal var playgroundDescription: Any { return description }
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 16/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
internal final class SubjectSubscriber<Downstream: Subject>
|
||||
: Subscriber,
|
||||
@@ -13,7 +15,7 @@ internal final class SubjectSubscriber<Downstream: Subject>
|
||||
CustomPlaygroundDisplayConvertible,
|
||||
Subscription
|
||||
{
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
private var downstreamSubject: Downstream?
|
||||
private var upstreamSubscription: Subscription?
|
||||
|
||||
@@ -23,6 +25,10 @@ internal final class SubjectSubscriber<Downstream: Subject>
|
||||
self.downstreamSubject = parent
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
internal func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard upstreamSubscription == nil, let subject = downstreamSubject else {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// Unreachable.swift
|
||||
// Unreachable
|
||||
//
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Copyright (c) 2017 Nikolai Vazquez
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
//
|
||||
|
||||
// All the credits go to https://github.com/nvzqz/Unreachable
|
||||
|
||||
/// An unreachable code path.
|
||||
///
|
||||
/// This can be used for whenever the compiler can't determine that a
|
||||
/// path is unreachable, such as dynamically terminating an iterator.
|
||||
@inline(__always)
|
||||
private func unsafeUnreachable() -> Never {
|
||||
return unsafeBitCast((), to: Never.self)
|
||||
}
|
||||
|
||||
/// Asserts that the code path is unreachable.
|
||||
///
|
||||
/// Calls `assertionFailure(_:file:line:)` in unoptimized builds and `unreachable()`
|
||||
/// otherwise.
|
||||
///
|
||||
/// - parameter message: The message to print. The default is
|
||||
/// "Encountered unreachable path".
|
||||
/// - parameter file: The file name to print with the message. The default is the file
|
||||
/// where this function is called.
|
||||
/// - parameter line: The line number to print with the message. The default is the line
|
||||
/// where this function is called.
|
||||
@inline(__always)
|
||||
internal func unreachable(
|
||||
_ message: @autoclosure () -> String = "Encountered unreachable path",
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) -> Never {
|
||||
var isDebug = false
|
||||
assert({ isDebug = true; return true }())
|
||||
if isDebug {
|
||||
fatalError(message(), file: file, line: line)
|
||||
} else {
|
||||
unsafeUnreachable()
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,8 @@ internal func APIViolationUnexpectedCompletion(file: StaticString = #file,
|
||||
fatalError("API Violation: received an unexpected completion", file: file, line: line)
|
||||
}
|
||||
|
||||
@inline(__always)
|
||||
internal func abstractMethod(file: StaticString = #file, line: UInt = #line) -> Never {
|
||||
unreachable("Abstract method call", file: file, line: line)
|
||||
fatalError("Abstract method call", file: file, line: line)
|
||||
}
|
||||
|
||||
extension Subscribers.Demand {
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
/// A subject that passes along values and completion.
|
||||
///
|
||||
/// Use a `PassthroughSubject` in unit tests when you want a publisher than can publish
|
||||
/// specific values on-demand during tests.
|
||||
public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
|
||||
private let _lock = unfairRecursiveLock()
|
||||
private let _lock = UnfairRecursiveLock.allocate()
|
||||
|
||||
private var _completion: Subscribers.Completion<Failure>?
|
||||
|
||||
@@ -28,6 +30,7 @@ public final class PassthroughSubject<Output, Failure: Error>: Subject {
|
||||
for subscription in _subscriptions {
|
||||
subscription._downstream = nil
|
||||
}
|
||||
_lock.deallocate()
|
||||
}
|
||||
|
||||
public func send(subscription: Subscription) {
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
/// and a publisher which sends any new values after the property value
|
||||
/// has been sent. New subscribers will receive the current value
|
||||
/// of the property first.
|
||||
/// Note that the `@Published` property is class-constrained.
|
||||
/// Use it with properties of classes, not with non-class types like structures.
|
||||
@propertyWrapper public struct Published<Value> {
|
||||
|
||||
/// Initialize the storage of the Published
|
||||
@@ -25,6 +27,7 @@
|
||||
value = wrappedValue
|
||||
}
|
||||
|
||||
/// A publisher for properties marked with the `@Published` attribute.
|
||||
public struct Publisher: OpenCombine.Publisher {
|
||||
|
||||
/// The kind of values published by this publisher.
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
//
|
||||
// Publishers.MapKeyPath.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Returns a publisher that publishes the values of a keyt path as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath: The key path of a property on `Output`
|
||||
/// - Returns: A publisher that publishes the value of the key path.
|
||||
public func map<Result>(
|
||||
_ keyPath: KeyPath<Output, Result>
|
||||
) -> Publishers.MapKeyPath<Self, Result> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath: keyPath
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a publisher that publishes the values of two key paths as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`
|
||||
/// - keyPath1: The key path of another property on `Output`
|
||||
/// - Returns: A publisher that publishes the values of two key paths as a tuple.
|
||||
public func map<Result0, Result1>(
|
||||
_ keyPath0: KeyPath<Output, Result0>,
|
||||
_ keyPath1: KeyPath<Output, Result1>
|
||||
) -> Publishers.MapKeyPath2<Self, Result0, Result1> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath0: keyPath0,
|
||||
keyPath1: keyPath1
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns a publisher that publishes the values of three key paths as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - keyPath0: The key path of a property on `Output`
|
||||
/// - keyPath1: The key path of another property on `Output`
|
||||
/// - keyPath2: The key path of a third property on `Output`
|
||||
/// - Returns: A publisher that publishes the values of three key paths as a tuple.
|
||||
public func map<Result0, Result1, Result2>(
|
||||
_ keyPath0: KeyPath<Output, Result0>,
|
||||
_ keyPath1: KeyPath<Output, Result1>,
|
||||
_ keyPath2: KeyPath<Output, Result2>
|
||||
) -> Publishers.MapKeyPath3<Self, Result0, Result1, Result2> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
keyPath0: keyPath0,
|
||||
keyPath1: keyPath1,
|
||||
keyPath2: keyPath2
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the value of a key path.
|
||||
public struct MapKeyPath<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath: KeyPath<Upstream.Output, Output>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes the values of two key paths as a tuple.
|
||||
public struct MapKeyPath2<Upstream: Publisher, Output0, Output1>: Publisher {
|
||||
|
||||
public typealias Output = (Output0, Output1)
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath0: KeyPath<Upstream.Output, Output0>
|
||||
|
||||
/// The key path of a second property to publish.
|
||||
public let keyPath1: KeyPath<Upstream.Output, Output1>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes the values of three key paths as a tuple.
|
||||
public struct MapKeyPath3<Upstream: Publisher, Output0, Output1, Output2>: Publisher {
|
||||
|
||||
public typealias Output = (Output0, Output1, Output2)
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The key path of a property to publish.
|
||||
public let keyPath0: KeyPath<Upstream.Output, Output0>
|
||||
|
||||
/// The key path of a second property to publish.
|
||||
public let keyPath1: KeyPath<Upstream.Output, Output1>
|
||||
|
||||
/// The key path of a third property to publish.
|
||||
public let keyPath2: KeyPath<Upstream.Output, Output2>
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath: KeyPath<Input, Output>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath<Upstream, Output>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath = parent.keyPath
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKey" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath", keyPath),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath2 {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath0: KeyPath<Input, Output0>
|
||||
|
||||
private let keyPath1: KeyPath<Input, Output1>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath2<Upstream, Output0, Output1>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath0 = parent.keyPath0
|
||||
self.keyPath1 = parent.keyPath1
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath0],
|
||||
input[keyPath: keyPath1]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKeys" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath0", keyPath0),
|
||||
("keyPath1", keyPath1),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.MapKeyPath3 {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let keyPath0: KeyPath<Input, Output0>
|
||||
|
||||
private let keyPath1: KeyPath<Input, Output1>
|
||||
|
||||
private let keyPath2: KeyPath<Input, Output2>
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.MapKeyPath3<Upstream, Output0, Output1, Output2>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.keyPath0 = parent.keyPath0
|
||||
self.keyPath1 = parent.keyPath1
|
||||
self.keyPath2 = parent.keyPath2
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
let output = (
|
||||
input[keyPath: keyPath0],
|
||||
input[keyPath: keyPath1],
|
||||
input[keyPath: keyPath2]
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "ValueForKeys" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("keyPath0", keyPath0),
|
||||
("keyPath1", keyPath1),
|
||||
("keyPath2", keyPath2),
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//
|
||||
// Publishers.AllSatisfy.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes a single Boolean value that indicates whether all received elements pass
|
||||
/// a given predicate.
|
||||
///
|
||||
/// When this publisher receives an element, it runs the predicate against
|
||||
/// the element. If the predicate returns `false`, the publisher produces a `false`
|
||||
/// value and finishes. If the upstream publisher finishes normally, this publisher
|
||||
/// produces a `true` value and finishes.
|
||||
/// As a `reduce`-style operator, this publisher produces at most one value.
|
||||
/// Backpressure note: Upon receiving any request greater than zero, this publisher
|
||||
/// requests unlimited elements from the upstream publisher.
|
||||
///
|
||||
/// - Parameter predicate: A closure that evaluates each received element.
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// - Returns: A publisher that publishes a Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public func allSatisfy(
|
||||
_ predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.AllSatisfy<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes a single Boolean value that indicates whether all received elements pass
|
||||
/// a given error-throwing predicate.
|
||||
///
|
||||
/// When this publisher receives an element, it runs the predicate against
|
||||
/// the element. If the predicate returns `false`, the publisher produces a `false`
|
||||
/// value and finishes. If the upstream publisher finishes normally, this publisher
|
||||
/// produces a `true` value and finishes. If the predicate throws an error,
|
||||
/// the publisher fails, passing the error to its downstream.
|
||||
/// As a `reduce`-style operator, this publisher produces at most one value.
|
||||
/// Backpressure note: Upon receiving any request greater than zero, this publisher
|
||||
/// requests unlimited elements from the upstream publisher.
|
||||
///
|
||||
/// - Parameter predicate: A closure that evaluates each received element.
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// The closure may throw, in which case the publisher cancels the upstream
|
||||
/// publisher and fails with the thrown error.
|
||||
/// - Returns: A publisher that publishes a Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public func tryAllSatisfy(
|
||||
_ predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryAllSatisfy<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a single Boolean value that indicates whether
|
||||
/// all received elements pass a given predicate.
|
||||
public struct AllSatisfy<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that evaluates each received element.
|
||||
///
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and finish.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that publishes a single Boolean value that indicates whether
|
||||
/// all received elements pass a given error-throwing predicate.
|
||||
public struct TryAllSatisfy<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that evaluates each received element.
|
||||
///
|
||||
/// Return `true` to continue, or `false` to cancel the upstream and complete.
|
||||
/// The closure may throw, in which case the publisher cancels the upstream
|
||||
/// publisher and fails with the thrown error.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.AllSatisfy {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: true, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if !reduce(newValue) {
|
||||
result = false
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "AllSatisfy" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryAllSatisfy {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: true, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try !reduce(newValue) {
|
||||
result = false
|
||||
return .finished
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "TryAllSatisfy" }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 18/09/2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension ConnectablePublisher {
|
||||
|
||||
/// Automates the process of connecting or disconnecting from this connectable
|
||||
@@ -45,7 +47,7 @@ extension Publishers {
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public final let upstream: Upstream
|
||||
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state = State.disconnected
|
||||
|
||||
@@ -53,6 +55,10 @@ extension Publishers {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Input == Output, Downstream.Failure == Failure
|
||||
{
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Publishers.Collect.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Collects all received elements, and emits a single array of the collection when
|
||||
/// the upstream publisher finishes.
|
||||
///
|
||||
/// If the upstream publisher fails with an error, this publisher forwards the error
|
||||
/// to the downstream receiver instead of sending its output.
|
||||
/// This publisher requests an unlimited number of elements from the upstream
|
||||
/// publisher. It only sends the collected array to its downstream after a request
|
||||
/// whose demand is greater than 0 items.
|
||||
/// Note: This publisher uses an unbounded amount of memory to store the received
|
||||
/// values.
|
||||
///
|
||||
/// - Returns: A publisher that collects all received items and returns them as
|
||||
/// an array upon completion.
|
||||
public func collect() -> Publishers.Collect<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that buffers items.
|
||||
public struct Collect<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = [Upstream.Output]
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == [Upstream.Output]
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Collect: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Collect {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
[Upstream.Output],
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Downstream.Input == [Upstream.Output],
|
||||
Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: [], reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result!.append(newValue)
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "Collect"
|
||||
}
|
||||
|
||||
override var customMirror: Mirror {
|
||||
let children: CollectionOfOne<Mirror.Child> = .init(("count", result!.count))
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// Publishers.Comparison.swift
|
||||
// OpenCombine
|
||||
//
|
||||
// Created by Ilija Puaca on 22/7/19.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Comparable {
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func min() -> Publishers.Comparison<Self> {
|
||||
return max(by: >)
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func max() -> Publishers.Comparison<Self> {
|
||||
return max(by: <)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, after it
|
||||
/// finishes.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
|
||||
/// `true` if they are in increasing order.
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func min(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
|
||||
) -> Publishers.Comparison<Self> {
|
||||
return max(by: { areInIncreasingOrder($1, $0) })
|
||||
}
|
||||
|
||||
/// Publishes the minimum value received from the upstream publisher, using the
|
||||
/// provided error-throwing closure to order the items.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
|
||||
/// and returns `true` if they are in increasing order. If this closure throws, the
|
||||
/// publisher terminates with a `Failure`.
|
||||
/// - Returns: A publisher that publishes the minimum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func tryMin(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) throws -> Bool
|
||||
) -> Publishers.TryComparison<Self> {
|
||||
return tryMax(by: { try areInIncreasingOrder($1, $0) })
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, using the
|
||||
/// provided ordering closure.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A closure that receives two elements and returns
|
||||
/// `true` if they are in increasing order.
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func max(
|
||||
by areInIncreasingOrder: @escaping (Output, Output) -> Bool
|
||||
) -> Publishers.Comparison<Self> {
|
||||
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
/// Publishes the maximum value received from the upstream publisher, using the
|
||||
/// provided error-throwing closure to order the items.
|
||||
///
|
||||
/// After this publisher receives a request for more than 0 items, it requests
|
||||
/// unlimited items from its upstream publisher.
|
||||
/// - Parameter areInIncreasingOrder: A throwing closure that receives two elements
|
||||
/// and returns `true` if they are in increasing order. If this closure throws, the
|
||||
/// publisher terminates with a `Failure`.
|
||||
/// - Returns: A publisher that publishes the maximum value received from the upstream
|
||||
/// publisher, after the upstream publisher finishes.
|
||||
public func tryMax(
|
||||
by areInIncreasingOrder: @escaping (Self.Output, Self.Output) throws -> Bool
|
||||
) -> Publishers.TryComparison<Self> {
|
||||
return .init(upstream: self, areInIncreasingOrder: areInIncreasingOrder)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is
|
||||
/// in increasing order from the previously-published item.
|
||||
public struct Comparison<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives two elements and returns `true` if they are in
|
||||
/// increasing order.
|
||||
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) -> Bool
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.areInIncreasingOrder = areInIncreasingOrder
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
areInIncreasingOrder: areInIncreasingOrder)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that republishes items from another publisher only if each new item is
|
||||
/// in increasing order from the previously-published item, and fails if the ordering
|
||||
/// logic throws an error.
|
||||
public struct TryComparison<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher that this publisher receives elements from.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure that receives two elements and returns `true` if they are in
|
||||
/// increasing order.
|
||||
public let areInIncreasingOrder: (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
areInIncreasingOrder:
|
||||
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.areInIncreasingOrder = areInIncreasingOrder
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
areInIncreasingOrder: areInIncreasingOrder)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Comparison {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output, Upstream.Output) -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
areInIncreasingOrder: @escaping (Upstream.Output, Upstream.Output) -> Bool
|
||||
) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if let result = self.result {
|
||||
if reduce(result, newValue) {
|
||||
self.result = newValue
|
||||
}
|
||||
} else {
|
||||
self.result = newValue
|
||||
}
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "Comparison"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryComparison {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output, Upstream.Output) throws -> Bool>
|
||||
where Downstream.Input == Upstream.Output, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
areInIncreasingOrder:
|
||||
@escaping (Upstream.Output, Upstream.Output) throws -> Bool
|
||||
) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if let result = self.result {
|
||||
if try reduce(result, newValue) {
|
||||
self.result = newValue
|
||||
}
|
||||
} else {
|
||||
self.result = newValue
|
||||
}
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String {
|
||||
return "TryComparison"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// Publishers.Contains.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher where Output: Equatable {
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element equal to the argument.
|
||||
///
|
||||
/// The contains publisher consumes all received elements until the upstream publisher
|
||||
/// produces a matching element. At that point, it emits `true` and finishes normally.
|
||||
/// If the upstream finishes normally without producing a matching element,
|
||||
/// this publisher emits `false`, then finishes.
|
||||
///
|
||||
/// - Parameter output: An element to match against.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func contains(_ output: Output) -> Publishers.Contains<Self> {
|
||||
return .init(upstream: self, output: output)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element that satisfies the predicate
|
||||
/// closure.
|
||||
///
|
||||
/// This operator consumes elements produced from the upstream publisher until
|
||||
/// the upstream publisher produces a matching element.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether the element satisfies the closure’s
|
||||
/// comparison logic.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func contains(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.ContainsWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Publishes a Boolean value upon receiving an element that satisfies
|
||||
/// the throwing predicate closure.
|
||||
///
|
||||
/// This operator consumes elements produced from the upstream publisher until
|
||||
/// the upstream publisher produces a matching element. If the closure throws,
|
||||
/// the stream fails with an error.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether the element satisfies the closure’s
|
||||
/// comparison logic.
|
||||
/// - Returns: A publisher that emits the Boolean value `true` when the upstream
|
||||
/// publisher emits a matching value.
|
||||
public func tryContains(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryContainsWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that emits a Boolean value when a specified element is received from
|
||||
/// its upstream publisher.
|
||||
public struct Contains<Upstream: Publisher>: Publisher
|
||||
where Upstream.Output: Equatable
|
||||
{
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The element to scan for in the upstream publisher.
|
||||
public let output: Upstream.Output
|
||||
|
||||
public init(upstream: Upstream, output: Upstream.Output) {
|
||||
self.upstream = upstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, output: output))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that emits a Boolean value upon receiving an element that satisfies
|
||||
/// the predicate closure.
|
||||
public struct ContainsWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether the publisher should consider an element
|
||||
/// as a match.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that emits a Boolean value upon receiving an element that satisfies
|
||||
/// the throwing predicate closure.
|
||||
public struct TryContainsWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Bool
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether this publisher should
|
||||
/// emit a `true` element.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Contains {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream, Upstream.Output, Bool, Upstream.Failure, Void>
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
private let output: Upstream.Output
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.output = output
|
||||
super.init(downstream: downstream, initial: false, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if newValue == output {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Contains" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Contains : Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.ContainsWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output, Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Upstream.Failure == Downstream.Failure, Downstream.Input == Bool
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: false, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "ContainsWhere" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryContainsWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output, Bool,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Downstream.Failure == Error, Downstream.Input == Bool
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: false, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = true
|
||||
return .finished
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "TryContainsWhere" }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,17 @@
|
||||
// Created by Joseph Spadafora on 6/25/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Publishes the number of elements received from the upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that consumes all elements until the upstream publisher
|
||||
/// finishes, then emits a single value with the total number of elements received.
|
||||
public func count() -> Publishers.Count<Self> {
|
||||
return Publishers.Count(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes the number of elements received
|
||||
@@ -37,58 +48,30 @@ extension Publishers {
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Downstream.Input == Output
|
||||
{
|
||||
let count = _Count<Upstream, Downstream>(downstream: subscriber)
|
||||
upstream.subscribe(count)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publisher {
|
||||
extension Publishers.Count: Equatable where Upstream: Equatable {}
|
||||
|
||||
/// Publishes the number of elements received from the upstream publisher.
|
||||
///
|
||||
/// - Returns: A publisher that consumes all elements until the upstream publisher
|
||||
/// finishes, then emits a single value with the total number of elements received.
|
||||
public func count() -> Publishers.Count<Self> {
|
||||
return Publishers.Count(upstream: self)
|
||||
}
|
||||
}
|
||||
|
||||
private final class _Count<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
Subscription
|
||||
where Downstream.Input == Int,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
typealias Output = Int
|
||||
typealias Failure = Downstream.Failure
|
||||
|
||||
private var _count = 0
|
||||
|
||||
var description: String { return "Count" }
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
downstream.receive(subscription: self)
|
||||
upstreamSubscription?.request(.unlimited)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
_count += 1
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
if case .finished = completion {
|
||||
_ = downstream.receive(_count)
|
||||
extension Publishers.Count {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream, Upstream.Output, Int, Failure, Void>
|
||||
where Downstream.Input == Int,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: 0, reduce: ())
|
||||
}
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result! += 1
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Count" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
//
|
||||
// Publishers.Drop.swift
|
||||
//
|
||||
//
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Omits the specified number of elements before republishing subsequent elements.
|
||||
///
|
||||
/// - Parameter count: The number of elements to omit.
|
||||
/// - Returns: A publisher that does not republish the first `count` elements.
|
||||
public func dropFirst(_ count: Int = 1) -> Publishers.Drop<Self> {
|
||||
return .init(upstream: self, count: count)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
/// A publisher that omits a specified number of elements before republishing
|
||||
/// later elements.
|
||||
public struct Drop<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The number of elements to drop.
|
||||
public let count: Int
|
||||
|
||||
public init(upstream: Upstream, count: Int) {
|
||||
self.upstream = upstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Upstream.Failure == Downstream.Failure,
|
||||
Upstream.Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, count: count)
|
||||
upstream.subscribe(inner)
|
||||
subscriber.receive(subscription: inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Drop: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Drop {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscription,
|
||||
Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: This class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var subscription: Subscription?
|
||||
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
|
||||
private var count: Int
|
||||
|
||||
fileprivate init(downstream: Downstream, count: Int) {
|
||||
self.downstream = downstream
|
||||
self.count = count
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard self.subscription == nil else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
self.subscription = subscription
|
||||
precondition(count >= 0, "count must not be negative")
|
||||
let demandToRequestFromUpstream = pendingDemand + count
|
||||
lock.unlock()
|
||||
if demandToRequestFromUpstream > 0 {
|
||||
subscription.request(demandToRequestFromUpstream)
|
||||
}
|
||||
}
|
||||
|
||||
func receive(_ input: Upstream.Output) -> Subscribers.Demand {
|
||||
// Combine doesn't lock here!
|
||||
if count > 0 {
|
||||
count -= 1
|
||||
return .none
|
||||
}
|
||||
return downstream.receive(input)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
// Combine doesn't lock here!
|
||||
subscription = nil
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
demand.assertNonZero()
|
||||
lock.lock()
|
||||
guard let subscription = self.subscription else {
|
||||
self.pendingDemand += demand
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
// Combine doesn't lock here!
|
||||
subscription?.cancel()
|
||||
subscription = nil
|
||||
}
|
||||
|
||||
var description: String { return "Drop" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
return Mirror(self, children: EmptyCollection())
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -65,11 +65,9 @@ extension Publishers {
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: { _ in .success(true) })
|
||||
upstream.receive(subscriber: inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,11 +91,9 @@ extension Publishers {
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.receive(subscriber: inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,175 +117,94 @@ extension Publishers {
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure,
|
||||
Output == Downstream.Input
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
let inner = Inner(downstream: subscriber, predicate: catching(predicate))
|
||||
upstream.receive(subscriber: inner)
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First: Equatable where Upstream: Equatable {}
|
||||
|
||||
private class _FirstWhere<Upstream: Publisher, Downstream: Subscriber>
|
||||
: OperatorSubscription<Downstream>,
|
||||
Subscription
|
||||
where Downstream.Input == Upstream.Output
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
typealias Failure = Upstream.Failure
|
||||
typealias Predicate = (Input) -> Result<Bool, Downstream.Failure>
|
||||
|
||||
// ┌──────────────────┐
|
||||
// ┌──────▶│ .pending(input) │───────────┐
|
||||
// │ └──────────────────┘ │
|
||||
// receive(input)│ │request(demand)
|
||||
// │ │
|
||||
// │ │
|
||||
// │ ▼
|
||||
// ┌──────────────────┐ ┌──────────────┐
|
||||
// ●───▶│.waitingForDemand │ │ .finished │
|
||||
// └──────────────────┘ └──────────────┘
|
||||
// │ ▲
|
||||
// │ │
|
||||
// │ │
|
||||
// request(demand)│ │receive(input)
|
||||
// │ ┌──────────────────────────┐ │
|
||||
// └───▶│ .downstreamHasRequested │──────┘
|
||||
// └──────────────────────────┘
|
||||
enum State {
|
||||
case waitingForDemand
|
||||
case pending(Input)
|
||||
case downstreamHasRequested
|
||||
case finished
|
||||
}
|
||||
|
||||
var predicate: Predicate?
|
||||
private var _state: State = .waitingForDemand
|
||||
|
||||
var isCompleted: Bool {
|
||||
return predicate == nil
|
||||
}
|
||||
|
||||
init(downstream: Downstream, predicate: @escaping Predicate) {
|
||||
self.predicate = predicate
|
||||
super.init(downstream: downstream)
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
upstreamSubscription = subscription
|
||||
subscription.request(.unlimited)
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
switch _state {
|
||||
case .pending, .finished:
|
||||
break
|
||||
case .downstreamHasRequested:
|
||||
_ifSatisfiesPredicate(input) {
|
||||
_state = .finished
|
||||
_sendDownstream(input)
|
||||
}
|
||||
case .waitingForDemand:
|
||||
_ifSatisfiesPredicate(input) {
|
||||
_state = .pending(input)
|
||||
}
|
||||
}
|
||||
return .none
|
||||
}
|
||||
|
||||
private func _ifSatisfiesPredicate(_ input: Input, _ onSuccess: () -> Void) {
|
||||
guard let predicate = self.predicate else { return }
|
||||
switch predicate(input) {
|
||||
case .success(true):
|
||||
onSuccess()
|
||||
case .success(false):
|
||||
return
|
||||
case .failure(let error):
|
||||
cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private func _sendDownstream(_ input: Input) {
|
||||
_ = downstream.receive(input)
|
||||
cancel()
|
||||
downstream.receive(completion: .finished)
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
precondition(demand > 0, "demand must not be zero")
|
||||
switch _state {
|
||||
case .waitingForDemand:
|
||||
_state = .downstreamHasRequested
|
||||
case .pending(let input):
|
||||
_state = .finished
|
||||
_sendDownstream(input)
|
||||
case .finished, .downstreamHasRequested:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func cancel() {
|
||||
predicate = nil
|
||||
upstreamSubscription?.cancel()
|
||||
upstreamSubscription = nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.First {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
var description: String { return "First" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion)
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = newValue
|
||||
return .finished
|
||||
}
|
||||
|
||||
override var description: String { return "First" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.FirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
: ReduceProducer<Downstream, Output, Output, Failure, (Output) -> Bool>
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
var description: String { return "TryFirst" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion)
|
||||
fileprivate init(downstream: Downstream, predicate: @escaping (Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = newValue
|
||||
return .finished
|
||||
} else {
|
||||
return .continue
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryFirst" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryFirstWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: _FirstWhere<Upstream, Downstream>,
|
||||
Subscriber,
|
||||
CustomStringConvertible
|
||||
where Upstream.Output == Downstream.Input,
|
||||
Downstream.Failure == Error
|
||||
: ReduceProducer<Downstream,
|
||||
Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output) throws -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
var description: String { return "TryFirstWhere" }
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
guard !isCompleted else { return }
|
||||
predicate = nil
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Output
|
||||
) -> PartialCompletion<Void, Error> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = newValue
|
||||
return .finished
|
||||
} else {
|
||||
return .continue
|
||||
}
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryFirstWhere" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
// Created by Eric Patey on 16.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from an upstream publisher into a new or existing
|
||||
@@ -91,7 +93,7 @@ extension Publishers.FlatMap {
|
||||
pausedChild: ChildSubscriber?
|
||||
)
|
||||
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
private let maxPublishers: Subscribers.Demand
|
||||
private let transform: (Input) -> Child
|
||||
|
||||
@@ -118,6 +120,10 @@ extension Publishers.FlatMap {
|
||||
self.transform = transform
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
final func cancel() {
|
||||
|
||||
let (upstreamToCancel, childrenToCancel) = lock
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// Publishers.Last.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/9/19.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Only publishes the last element of a stream, after the stream finishes.
|
||||
/// - Returns: A publisher that only publishes the last element of a stream.
|
||||
public func last() -> Publishers.Last<Self> {
|
||||
return .init(upstream: self)
|
||||
}
|
||||
|
||||
/// Only publishes the last element of a stream that satisfies a predicate closure,
|
||||
/// after the stream finishes.
|
||||
///
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the last element satisfying
|
||||
/// the given predicate.
|
||||
public func last(
|
||||
where predicate: @escaping (Output) -> Bool
|
||||
) -> Publishers.LastWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
|
||||
/// Only publishes the last element of a stream that satisfies an error-throwing
|
||||
/// predicate closure, after the stream finishes.
|
||||
///
|
||||
/// If the predicate closure throws, the publisher fails with the thrown error.
|
||||
/// - Parameter predicate: A closure that takes an element as its parameter and
|
||||
/// returns a Boolean value indicating whether to publish the element.
|
||||
/// - Returns: A publisher that only publishes the last element satisfying
|
||||
/// the given predicate.
|
||||
public func tryLast(
|
||||
where predicate: @escaping (Output) throws -> Bool
|
||||
) -> Publishers.TryLastWhere<Self> {
|
||||
return .init(upstream: self, predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that only publishes the last element of a stream,
|
||||
/// after the stream finishes.
|
||||
public struct Last<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
public init(upstream: Upstream) {
|
||||
self.upstream = upstream
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the last element of a stream that satisfies
|
||||
/// a predicate closure, once the stream finishes.
|
||||
public struct LastWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Failure == Downstream.Failure, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that only publishes the last element of a stream that satisfies
|
||||
/// an error-throwing predicate closure, once the stream finishes.
|
||||
public struct TryLastWhere<Upstream: Publisher>: Publisher {
|
||||
|
||||
public typealias Output = Upstream.Output
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The error-throwing closure that determines whether to publish an element.
|
||||
public let predicate: (Upstream.Output) throws -> Bool
|
||||
|
||||
public init(upstream: Upstream, predicate: @escaping (Output) throws -> Bool) {
|
||||
self.upstream = upstream
|
||||
self.predicate = predicate
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Downstream.Failure == Error, Output == Downstream.Input
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, predicate: predicate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Last: Equatable where Upstream: Equatable {}
|
||||
|
||||
extension Publishers.Last {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
Void>
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: ())
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = newValue
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Last" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.LastWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
if reduce(newValue) {
|
||||
result = newValue
|
||||
}
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "LastWhere" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryLastWhere {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Upstream.Output,
|
||||
Upstream.Failure,
|
||||
(Upstream.Output) throws -> Bool>
|
||||
where Upstream.Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
fileprivate init(downstream: Downstream,
|
||||
predicate: @escaping (Upstream.Output) throws -> Bool) {
|
||||
super.init(downstream: downstream, initial: nil, reduce: predicate)
|
||||
}
|
||||
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
if try reduce(newValue) {
|
||||
result = newValue
|
||||
}
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryLastWhere" }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Anton Nazarov on 25.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms all elements from the upstream publisher with a provided closure.
|
||||
@@ -187,7 +189,7 @@ extension Publishers.TryMap {
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
@@ -197,6 +199,10 @@ extension Publishers.TryMap {
|
||||
self.map = map
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
${template_header}
|
||||
//
|
||||
// Publishers.MapKeyPath.swift.gyb
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 03/10/2019.
|
||||
//
|
||||
|
||||
%{
|
||||
from gyb_opencombine_support import (
|
||||
suffix_variadic,
|
||||
list_with_suffix_variadic
|
||||
)
|
||||
|
||||
instantiations = [(1, '', ''),
|
||||
(2, 'two', 'second '),
|
||||
(3, 'three', 'third ')]
|
||||
|
||||
def key_path_var(index, arity):
|
||||
return suffix_variadic('keyPath', index, arity)
|
||||
|
||||
def make_publisher_name(arity):
|
||||
return suffix_variadic('MapKeyPath', arity, arity)
|
||||
|
||||
def make_output_types(arity):
|
||||
return list_with_suffix_variadic('Output', arity)
|
||||
}%
|
||||
extension Publisher {
|
||||
% for arity, cardinal, _ in instantiations:
|
||||
% result_types = list_with_suffix_variadic('Result', arity)
|
||||
% cs_result_types = ', '.join(result_types)
|
||||
%
|
||||
% method_args = \
|
||||
% ['_ {}: KeyPath<Output, {}>'.format(key_path_var(i, arity), result_types[i]) \
|
||||
% for i in range(arity)]
|
||||
% method_args_joined = ',\n '.join(method_args)
|
||||
%
|
||||
% init_args = ['{}: {}'.format(key_path_var(i, arity), key_path_var(i, arity)) \
|
||||
% for i in range(arity)]
|
||||
% init_args_joined = ',\n '.join(init_args)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
%
|
||||
% doc_cardinal = 'a keyt path' if arity == 1 else cardinal + ' key paths'
|
||||
|
||||
/// Returns a publisher that publishes the values of ${doc_cardinal} as a tuple.
|
||||
///
|
||||
/// - Parameters:
|
||||
% for i in range(arity):
|
||||
% ordinal = 'another ' if i == 1 else 'a ' + instantiations[i][2]
|
||||
/// - ${key_path_var(i, arity)}: The key path of ${ordinal}property on `Output`
|
||||
% end
|
||||
%
|
||||
% doc_comment_suffix = 'value of the key path' \
|
||||
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
|
||||
/// - Returns: A publisher that publishes the ${doc_comment_suffix}.
|
||||
public func map<${cs_result_types}>(
|
||||
${method_args_joined}
|
||||
) -> Publishers.${publisher_name}<Self, ${cs_result_types}> {
|
||||
return .init(
|
||||
upstream: self,
|
||||
${init_args_joined}
|
||||
)
|
||||
}
|
||||
% end
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
% for arity, cardinal, ordinal in instantiations:
|
||||
%
|
||||
% doc_comment_suffix = 'value of a key path' \
|
||||
% if arity == 1 else 'values of {} key paths as a tuple'.format(cardinal)
|
||||
%
|
||||
% output_types = make_output_types(arity)
|
||||
% cs_output_types = ', '.join(output_types)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
|
||||
/// A publisher that publishes the ${doc_comment_suffix}.
|
||||
public struct ${publisher_name}<Upstream: Publisher, ${cs_output_types}>: Publisher {
|
||||
|
||||
% if arity != 1:
|
||||
public typealias Output = (${cs_output_types})
|
||||
|
||||
% end
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
% for i in range(arity):
|
||||
% ordinal = instantiations[i][2]
|
||||
/// The key path of a ${ordinal}property to publish.
|
||||
public let ${key_path_var(i, arity)}: KeyPath<Upstream.Output, ${output_types[i]}>
|
||||
|
||||
% end
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber, parent: self))
|
||||
}
|
||||
}
|
||||
% end
|
||||
}
|
||||
% for arity, _, _ in instantiations:
|
||||
% output_types = make_output_types(arity)
|
||||
% cs_output_types = ', '.join(output_types)
|
||||
%
|
||||
% publisher_name = make_publisher_name(arity)
|
||||
|
||||
extension Publishers.${publisher_name} {
|
||||
|
||||
private struct Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Input == Output, Downstream.Failure == Upstream.Failure
|
||||
{
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
% for i in range(arity):
|
||||
private let ${key_path_var(i, arity)}: KeyPath<Input, ${output_types[i]}>
|
||||
|
||||
% end
|
||||
let combineIdentifier = CombineIdentifier()
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
parent: Publishers.${publisher_name}<Upstream, ${cs_output_types}>
|
||||
) {
|
||||
self.downstream = downstream
|
||||
% for i in range(arity):
|
||||
self.${key_path_var(i, arity)} = parent.${key_path_var(i, arity)}
|
||||
% end
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
% output_components = \
|
||||
% ['input[keyPath: {}]'.format(key_path_var(i, arity)) for i in range(arity)]
|
||||
% output_components_joined = ',\n '.join(output_components)
|
||||
let output = (
|
||||
${output_components_joined}
|
||||
)
|
||||
return downstream.receive(output)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
% inner_description = 'ValueForKey' + ('' if arity == 1 else 's')
|
||||
var description: String { return "${inner_description}" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
% for i in range(arity):
|
||||
("${key_path_var(i, arity)}", ${key_path_var(i, arity)}),
|
||||
% end
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
% end
|
||||
@@ -5,8 +5,19 @@
|
||||
// Created by Sergej Jaskiewicz on 14.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure to create a subject that delivers elements to subscribers.
|
||||
///
|
||||
/// Use a multicast publisher when you have multiple downstream subscribers, but you
|
||||
/// want upstream publishers to only process one `receive(_:)` call per event.
|
||||
/// In contrast with `multicast(subject:)`, this method produces a publisher that
|
||||
/// creates a separate Subject for each subscriber.
|
||||
///
|
||||
/// - Parameter createSubject: A closure to create a new Subject each time
|
||||
/// a subscriber attaches to the multicast publisher.
|
||||
public func multicast<SubjectType: Subject>(
|
||||
_ createSubject: @escaping () -> SubjectType
|
||||
) -> Publishers.Multicast<Self, SubjectType>
|
||||
@@ -15,6 +26,14 @@ extension Publisher {
|
||||
return Publishers.Multicast(upstream: self, createSubject: createSubject)
|
||||
}
|
||||
|
||||
/// Provides a subject to deliver elements to multiple subscribers.
|
||||
///
|
||||
/// Use a multicast publisher when you have multiple downstream subscribers, but you
|
||||
/// want upstream publishers to only process one `receive(_:)` call per event.
|
||||
/// In contrast with `multicast(_:)`, this method produces a publisher shares
|
||||
/// the provided Subject among all the downstream subscribers.
|
||||
///
|
||||
/// - Parameter subject: A subject to deliver elements to downstream subscribers.
|
||||
public func multicast<SubjectType: Subject>(
|
||||
subject: SubjectType
|
||||
) -> Publishers.Multicast<Self, SubjectType>
|
||||
@@ -26,6 +45,7 @@ extension Publisher {
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that uses a subject to deliver elements to multiple subscribers.
|
||||
public final class Multicast<Upstream: Publisher, SubjectType: Subject>
|
||||
: ConnectablePublisher
|
||||
where Upstream.Failure == SubjectType.Failure,
|
||||
@@ -37,11 +57,14 @@ extension Publishers {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// A closure to create a new Subject each time a subscriber attaches
|
||||
/// to the multicast publisher.
|
||||
public let createSubject: () -> SubjectType
|
||||
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var subject: SubjectType?
|
||||
|
||||
@@ -58,11 +81,22 @@ extension Publishers {
|
||||
return subject
|
||||
}
|
||||
|
||||
/// Creates a multicast publisher that applies a closure to create a subject
|
||||
/// that delivers elements to subscribers.
|
||||
///
|
||||
/// - Parameter upstream: The publisher from which this publisher receives
|
||||
/// elements.
|
||||
/// - Parameter createSubject: A closure to create a new Subject each time
|
||||
/// a subscriber attaches to the multicast publisher.
|
||||
public init(upstream: Upstream, createSubject: @escaping () -> SubjectType) {
|
||||
self.upstream = upstream
|
||||
self.createSubject = createSubject
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where SubjectType.Failure == Downstream.Failure,
|
||||
SubjectType.Output == Downstream.Input
|
||||
@@ -100,7 +134,7 @@ extension Publishers.Multicast {
|
||||
case terminal
|
||||
}
|
||||
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var state: State
|
||||
|
||||
@@ -109,6 +143,10 @@ extension Publishers.Multicast {
|
||||
state = .ready(upstream: parent.upstream, downstream: downstream)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
fileprivate var description: String { return "Multicast" }
|
||||
|
||||
fileprivate var customMirror: Mirror {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 16.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that prints log messages for all publishing events, optionally
|
||||
@@ -88,7 +90,7 @@ extension Publishers.Print {
|
||||
private let prefix: String
|
||||
private var stream: PrintTarget?
|
||||
private var subscription: Subscription?
|
||||
private let lock = unfairLock()
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
init(downstream: Downstream, prefix: String, stream: TextOutputStream?) {
|
||||
self.downstream = downstream
|
||||
@@ -96,6 +98,10 @@ extension Publishers.Print {
|
||||
self.stream = stream.map(PrintTarget.init)
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
log("\(prefix)receive subscription: (\(subscription))")
|
||||
lock.do {
|
||||
@@ -150,7 +156,7 @@ extension Publishers.Print {
|
||||
if var stream = stream {
|
||||
Swift.print(text, to: &stream)
|
||||
} else {
|
||||
Swift.print("", text)
|
||||
Swift.print(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
//
|
||||
// Publishers.Reduce.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 09.10.2019.
|
||||
//
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Applies a closure that accumulates each element of a stream and publishes
|
||||
/// a final result upon completion.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value the closure receives the first time it is called.
|
||||
/// - nextPartialResult: A closure that takes the previously-accumulated value and
|
||||
/// the next element from the upstream publisher to produce a new value.
|
||||
/// - Returns: A publisher that applies the closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public func reduce<Accumulator>(
|
||||
_ initialResult: Accumulator,
|
||||
_ nextPartialResult: @escaping (Accumulator, Output) -> Accumulator
|
||||
) -> Publishers.Reduce<Self, Accumulator> {
|
||||
return .init(upstream: self,
|
||||
initial: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
|
||||
/// Applies an error-throwing closure that accumulates each element of a stream and
|
||||
/// publishes a final result upon completion.
|
||||
///
|
||||
/// If the closure throws an error, the publisher fails, passing the error
|
||||
/// to its subscriber.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The value the closure receives the first time it is called.
|
||||
/// - nextPartialResult: An error-throwing closure that takes
|
||||
/// the previously-accumulated value and the next element from the upstream
|
||||
/// publisher to produce a new value.
|
||||
/// - Returns: A publisher that applies the closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public func tryReduce<Accumulator>(
|
||||
_ initialResult: Accumulator,
|
||||
_ nextPartialResult: @escaping (Accumulator, Output) throws -> Accumulator
|
||||
) -> Publishers.TryReduce<Self, Accumulator> {
|
||||
return .init(upstream: self,
|
||||
initial: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that applies a closure to all received elements and produces
|
||||
/// an accumulated value when the upstream publisher finishes.
|
||||
public struct Reduce<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The initial value provided on the first invocation of the closure.
|
||||
public let initial: Output
|
||||
|
||||
/// A closure that takes the previously-accumulated value and the next element
|
||||
/// from the upstream publisher to produce a new value.
|
||||
public let nextPartialResult: (Output, Upstream.Output) -> Output
|
||||
|
||||
public init(upstream: Upstream,
|
||||
initial: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
|
||||
self.upstream = upstream
|
||||
self.initial = initial
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
initial: initial,
|
||||
reduce: nextPartialResult)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
|
||||
/// A publisher that applies an error-throwing closure to all received elements and
|
||||
/// produces an accumulated value when the upstream publisher finishes.
|
||||
public struct TryReduce<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
/// The publisher from which this publisher receives elements.
|
||||
public let upstream: Upstream
|
||||
|
||||
/// The initial value provided on the first invocation of the closure.
|
||||
public let initial: Output
|
||||
|
||||
/// An error-throwing closure that takes the previously-accumulated value and
|
||||
/// the next element from the upstream to produce a new value.
|
||||
///
|
||||
/// If this closure throws an error, the publisher fails and passes the error
|
||||
/// to its subscriber.
|
||||
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
initial: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.initial = initial
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
let inner = Inner(downstream: subscriber,
|
||||
initial: initial,
|
||||
reduce: nextPartialResult)
|
||||
upstream.subscribe(inner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Reduce {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output, Upstream.Output) -> Output>
|
||||
where Downstream.Input == Output, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
result = reduce(result!, newValue)
|
||||
return .continue
|
||||
}
|
||||
|
||||
override var description: String { return "Reduce" }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryReduce {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: ReduceProducer<Downstream,
|
||||
Upstream.Output,
|
||||
Output,
|
||||
Upstream.Failure,
|
||||
(Output, Upstream.Output) throws -> Output>
|
||||
where Downstream.Input == Output, Downstream.Failure == Error
|
||||
{
|
||||
override func receive(
|
||||
newValue: Upstream.Output
|
||||
) -> PartialCompletion<Void, Downstream.Failure> {
|
||||
do {
|
||||
result = try reduce(result!, newValue)
|
||||
return .continue
|
||||
} catch {
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
override var description: String { return "TryReduce" }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Bogdan Vlad on 8/29/19.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
/// Replaces any errors in the stream with the provided element.
|
||||
///
|
||||
@@ -81,13 +83,17 @@ extension Publishers.ReplaceError {
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
private var terminated = false
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var lock = unfairLock()
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, output: Upstream.Output) {
|
||||
self.downstream = downstream
|
||||
self.output = output
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
//
|
||||
// Publishers.Scan.swift
|
||||
//
|
||||
// Created by Eric Patey on 26.08.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publisher {
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
/// to a closure along with the last value returned by the closure.
|
||||
///
|
||||
/// let pub = (0...5)
|
||||
/// .publisher
|
||||
/// .scan(0, { return $0 + $1 })
|
||||
/// .sink(receiveValue: { print ("\($0)", terminator: " ") })
|
||||
/// // Prints "0 1 3 6 10 15 ".
|
||||
///
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - initialResult: The previous result returned by the `nextPartialResult`
|
||||
/// closure.
|
||||
/// - nextPartialResult: A closure that takes as its arguments the previous value
|
||||
/// returned by the closure and the next element emitted from the upstream
|
||||
/// publisher.
|
||||
/// - Returns: A publisher that transforms elements by applying a closure that
|
||||
/// receives its previous return value and the next element from the upstream
|
||||
/// publisher.
|
||||
public func scan<Result>(
|
||||
_ initialResult: Result,
|
||||
_ nextPartialResult: @escaping (Result, Output) -> Result
|
||||
) -> Publishers.Scan<Self, Result> {
|
||||
return .init(upstream: self,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
|
||||
/// Transforms elements from the upstream publisher by providing the current element
|
||||
/// to an error-throwing closure along with the last value returned by the closure.
|
||||
///
|
||||
/// If the closure throws an error, the publisher fails with the error.
|
||||
/// - Parameters:
|
||||
/// - initialResult: The previous result returned by the `nextPartialResult`
|
||||
/// closure.
|
||||
/// - nextPartialResult: An error-throwing closure that takes as its arguments the
|
||||
/// previous value returned by the closure and the next element emitted from the
|
||||
/// upstream publisher.
|
||||
/// - Returns: A publisher that transforms elements by applying a closure that
|
||||
/// receives its previous return value and the next element from the upstream
|
||||
/// publisher.
|
||||
public func tryScan<Result>(
|
||||
_ initialResult: Result,
|
||||
_ nextPartialResult: @escaping (Result, Output) throws -> Result
|
||||
) -> Publishers.TryScan<Self, Result> {
|
||||
return .init(upstream: self,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult)
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers {
|
||||
|
||||
public struct Scan<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Upstream.Failure
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let initialResult: Output
|
||||
|
||||
public let nextPartialResult: (Output, Upstream.Output) -> Output
|
||||
|
||||
public init(upstream: Upstream,
|
||||
initialResult: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) -> Output) {
|
||||
self.upstream = upstream
|
||||
self.initialResult = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult))
|
||||
}
|
||||
}
|
||||
|
||||
public struct TryScan<Upstream: Publisher, Output>: Publisher {
|
||||
|
||||
public typealias Failure = Error
|
||||
|
||||
public let upstream: Upstream
|
||||
|
||||
public let initialResult: Output
|
||||
|
||||
public let nextPartialResult: (Output, Upstream.Output) throws -> Output
|
||||
|
||||
public init(
|
||||
upstream: Upstream,
|
||||
initialResult: Output,
|
||||
nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output
|
||||
) {
|
||||
self.upstream = upstream
|
||||
self.initialResult = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
public func receive<Downstream: Subscriber>(subscriber: Downstream)
|
||||
where Output == Downstream.Input, Downstream.Failure == Error
|
||||
{
|
||||
upstream.subscribe(Inner(downstream: subscriber,
|
||||
initialResult: initialResult,
|
||||
nextPartialResult: nextPartialResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.Scan {
|
||||
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Upstream.Failure == Downstream.Failure
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
// Combine doesn't use any locking here.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let nextPartialResult: (Downstream.Input, Input) -> Downstream.Input
|
||||
|
||||
private var result: Downstream.Input
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
initialResult: Downstream.Input,
|
||||
nextPartialResult: @escaping (Downstream.Input, Input) -> Downstream.Input
|
||||
)
|
||||
{
|
||||
self.downstream = downstream
|
||||
self.result = initialResult
|
||||
self.nextPartialResult = nextPartialResult
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
downstream.receive(subscription: subscription)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
result = nextPartialResult(result, input)
|
||||
return downstream.receive(result)
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
downstream.receive(completion: completion)
|
||||
}
|
||||
|
||||
var description: String { return "Scan" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("result", result)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
|
||||
extension Publishers.TryScan {
|
||||
private final class Inner<Downstream: Subscriber>
|
||||
: Subscriber,
|
||||
Subscription,
|
||||
CustomStringConvertible,
|
||||
CustomReflectable,
|
||||
CustomPlaygroundDisplayConvertible
|
||||
where Downstream.Failure == Error
|
||||
{
|
||||
// NOTE: this class has been audited for thread safety.
|
||||
|
||||
typealias Input = Upstream.Output
|
||||
|
||||
typealias Failure = Upstream.Failure
|
||||
|
||||
private let downstream: Downstream
|
||||
|
||||
private let nextPartialResult:
|
||||
(Downstream.Input, Input) throws -> Downstream.Input
|
||||
|
||||
private var result: Downstream.Input
|
||||
|
||||
private var status = SubscriptionStatus.awaitingSubscription
|
||||
|
||||
private let lock = UnfairLock.allocate()
|
||||
|
||||
private var finished = false
|
||||
|
||||
fileprivate init(
|
||||
downstream: Downstream,
|
||||
initialResult: Downstream.Input,
|
||||
nextPartialResult:
|
||||
@escaping (Downstream.Input, Input) throws -> Downstream.Input
|
||||
) {
|
||||
self.downstream = downstream
|
||||
self.nextPartialResult = nextPartialResult
|
||||
self.result = initialResult
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
lock.lock()
|
||||
guard case .awaitingSubscription = status else {
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
return
|
||||
}
|
||||
status = .subscribed(subscription)
|
||||
lock.unlock()
|
||||
downstream.receive(subscription: self)
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
do {
|
||||
result = try nextPartialResult(result, input)
|
||||
return downstream.receive(result)
|
||||
} catch {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return .none
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
downstream.receive(completion: .failure(error))
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Upstream.Failure>) {
|
||||
// Combine doesn't use locking in this method!
|
||||
guard case .subscribed = status else {
|
||||
return
|
||||
}
|
||||
downstream.receive(completion: completion.eraseError())
|
||||
}
|
||||
|
||||
func request(_ demand: Subscribers.Demand) {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
lock.unlock()
|
||||
subscription.request(demand)
|
||||
}
|
||||
|
||||
func cancel() {
|
||||
lock.lock()
|
||||
guard case let .subscribed(subscription) = status else {
|
||||
lock.unlock()
|
||||
return
|
||||
}
|
||||
status = .terminal
|
||||
lock.unlock()
|
||||
subscription.cancel()
|
||||
}
|
||||
|
||||
var description: String { return "TryScan" }
|
||||
|
||||
var customMirror: Mirror {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
let children: [Mirror.Child] = [
|
||||
("downstream", downstream),
|
||||
("status", status),
|
||||
("result", result)
|
||||
]
|
||||
return Mirror(self, children: children)
|
||||
}
|
||||
|
||||
var playgroundDescription: Any { return description }
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 19.06.2019.
|
||||
//
|
||||
|
||||
import COpenCombineHelpers
|
||||
|
||||
extension Publishers {
|
||||
|
||||
/// A publisher that publishes a given sequence of elements.
|
||||
@@ -62,7 +64,7 @@ extension Publishers.Sequence {
|
||||
private var next: Element?
|
||||
private var pendingDemand = Subscribers.Demand.none
|
||||
private var recursion = false
|
||||
private var lock = unfairLock()
|
||||
private var lock = UnfairLock.allocate()
|
||||
|
||||
fileprivate init(downstream: Downstream, sequence: Elements) {
|
||||
self.sequence = sequence
|
||||
@@ -71,6 +73,10 @@ extension Publishers.Sequence {
|
||||
next = iterator.next()
|
||||
}
|
||||
|
||||
deinit {
|
||||
lock.deallocate()
|
||||
}
|
||||
|
||||
var description: String {
|
||||
return sequence.map(String.init(describing:)) ?? "Sequence"
|
||||
}
|
||||
|
||||
@@ -30,12 +30,18 @@ extension Subscribers {
|
||||
}
|
||||
|
||||
/// Requests as many values as the `Publisher` can produce.
|
||||
public static let unlimited = Demand(rawValue: .max)
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static var unlimited: Demand {
|
||||
return Demand(rawValue: .max)
|
||||
}
|
||||
|
||||
/// A demand for no items.
|
||||
///
|
||||
/// This is equivalent to `Demand.max(0)`.
|
||||
public static let none = Demand.max(0)
|
||||
@inline(__always)
|
||||
@inlinable
|
||||
public static var none: Demand { return .max(0) }
|
||||
|
||||
/// Limits the maximum number of values.
|
||||
/// The `Publisher` may send fewer than the requested number.
|
||||
|
||||
@@ -388,7 +388,7 @@ final class DispatchQueueSchedulerTests: XCTestCase {
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
|
||||
// Wait for the background scheduler to execute the work.
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 0.1), .success)
|
||||
XCTAssertEqual(group.wait(timeout: .now() + 5.0), .success)
|
||||
|
||||
XCTAssertFalse(didExecuteMainAction, "action should be executed asynchronously")
|
||||
XCTAssertTrue(didExecuteBackgroundAction.value)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// CleaningUpSubscriber.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 17.10.2019.
|
||||
//
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CleaningUpSubscriber<Input, Failure: Error>: Subscriber {
|
||||
|
||||
private(set) var subscription: Subscription?
|
||||
|
||||
private let onDeinit: () -> Void
|
||||
|
||||
init(onDeinit: @escaping () -> Void) {
|
||||
self.onDeinit = onDeinit
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit()
|
||||
}
|
||||
|
||||
func receive(subscription: Subscription) {
|
||||
self.subscription = subscription
|
||||
}
|
||||
|
||||
func receive(_ input: Input) -> Subscribers.Demand {
|
||||
return .none
|
||||
}
|
||||
|
||||
func receive(completion: Subscribers.Completion<Failure>) {
|
||||
subscription = nil
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ import OpenCombine
|
||||
/// `CustomSubscription`, `CustomPublisherBase` and `TrackingSubscriberBase`.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
class OperatorTestHelper<SourceValue: Equatable,
|
||||
SourceError: Error,
|
||||
SourcePublisher,
|
||||
Sut: Publisher>
|
||||
where Sut.Output: Equatable,
|
||||
SourcePublisher: CustomPublisherBase<SourceValue, TestingError>
|
||||
where SourcePublisher: CustomPublisherBase<SourceValue, SourceError>
|
||||
{
|
||||
typealias Value = Sut.Output
|
||||
typealias Failure = Sut.Failure
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// TestLifecycle.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
func testLifecycle<UpstreamOutput, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
sendValue valueToBeSent: UpstreamOutput,
|
||||
cancellingSubscriptionReleasesSubscriber: Bool,
|
||||
_ makeOperator: (PassthroughSubject<UpstreamOutput, TestingError>) -> Operator
|
||||
) throws {
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
// Lifecycle test #1
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber =
|
||||
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty,
|
||||
"Lifecycle test #1: thesubscriber's history should be empty",
|
||||
file: file,
|
||||
line: line)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(valueToBeSent)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
}
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
1,
|
||||
"""
|
||||
Lifecycle test #1: deinit should be called, because \
|
||||
the subscription has completed
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
0,
|
||||
"""
|
||||
Lifecycle test #1: deinit should not be called
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
// Lifecycle test #2
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber =
|
||||
TrackingSubscriberBase<Operator.Output, Operator.Failure>(onDeinit: onDeinit)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter,
|
||||
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
|
||||
"""
|
||||
Lifecycle test #2: deinit should not be called, \
|
||||
because the subscription is never cancelled
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
// Lifecycle test #3
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber = TrackingSubscriberBase<Operator.Output, Operator.Failure>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(valueToBeSent)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter,
|
||||
cancellingSubscriptionReleasesSubscriber ? 1 : 0,
|
||||
"""
|
||||
Lifecycle test #3: deinit should not be called, \
|
||||
because the subscription is not cancelled yet
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
try XCTUnwrap(subscription, file: file, line: line).cancel()
|
||||
|
||||
if cancellingSubscriptionReleasesSubscriber {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
2,
|
||||
"""
|
||||
Lifecycle test #3: deinit should be called, because
|
||||
the subscription has been cancelled
|
||||
""",
|
||||
file: file,
|
||||
line: line)
|
||||
} else {
|
||||
XCTAssertEqual(deinitCounter,
|
||||
0,
|
||||
"Lifecycle test #3: deinit should not be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
// Lifecycle test #4
|
||||
|
||||
var subscriberDestroyed = false
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<UpstreamOutput, TestingError>()
|
||||
let operatorPublisher = makeOperator(passthrough)
|
||||
let emptySubscriber = CleaningUpSubscriber<Operator.Output, Operator.Failure> {
|
||||
subscriberDestroyed = true
|
||||
}
|
||||
operatorPublisher.subscribe(emptySubscriber)
|
||||
passthrough.send(completion: .finished)
|
||||
}
|
||||
|
||||
XCTAssertTrue(subscriberDestroyed,
|
||||
"Lifecycle test #4: deinit should be called",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
@@ -13,9 +13,7 @@ import Combine
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
func childrenIsEmpty(_ mirror: Mirror) -> Bool {
|
||||
return mirror.children.isEmpty
|
||||
}
|
||||
let childrenIsEmpty: (Mirror) -> Bool = { $0.children.isEmpty }
|
||||
|
||||
enum ExpectedMirrorChildValue: Equatable, ExpressibleByStringLiteral {
|
||||
case anything
|
||||
@@ -56,6 +54,18 @@ func expectedChildren(_ expectedChildren: (String?, ExpectedMirrorChildValue)...
|
||||
}
|
||||
}
|
||||
|
||||
func reduceLikeOperatorMirror(file: StaticString = #file,
|
||||
line: UInt = #line) -> (Mirror) -> Bool {
|
||||
return expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriberBase")),
|
||||
("result", .anything),
|
||||
("initial", .anything),
|
||||
("status", .contains("awaitingSubscription")),
|
||||
file: file,
|
||||
line: line
|
||||
)
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
file: StaticString = #file,
|
||||
@@ -66,7 +76,7 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
customMirror customMirrorPredicate: ((Mirror) -> Bool)?,
|
||||
playgroundDescription: String,
|
||||
_ makeOperator: (CustomConnectablePublisherBase<Output, Failure>) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
) throws {
|
||||
let publisher = CustomConnectablePublisherBase<Output, Failure>(subscription: nil)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Operator.Failure>()
|
||||
@@ -87,10 +97,16 @@ internal func testReflection<Output, Failure: Error, Operator: Publisher>(
|
||||
|
||||
if let customMirrorPredicate = customMirrorPredicate {
|
||||
XCTAssert(customMirrorPredicate(customMirror),
|
||||
"customMirror doesn't satisfy the predicate",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertFalse(erasedSubscriber is CustomDebugStringConvertible,
|
||||
"subscriber shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
XCTAssertEqual(
|
||||
((erasedSubscriber as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
@@ -135,6 +151,11 @@ internal func testSubscriptionReflection<Sut: Publisher>(
|
||||
line: line)
|
||||
}
|
||||
|
||||
XCTAssertFalse(subscription is CustomDebugStringConvertible,
|
||||
"subscriber shouldn't conform to CustomDebugStringConvertible",
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
XCTAssertEqual(
|
||||
((subscription as? CustomPlaygroundDisplayConvertible)?
|
||||
.playgroundDescription as? String),
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
// Created by Sergej Jaskiewicz on 11.06.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
@@ -37,36 +39,16 @@ typealias TrackingSubscriber = TrackingSubscriberBase<Int, TestingError>
|
||||
/// is considered equal to any other subscription no matter what the subscription object
|
||||
/// actually is.
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
final class TrackingSubscriberBase<Value, Failure: Error>
|
||||
: Subscriber,
|
||||
CustomStringConvertible
|
||||
{
|
||||
|
||||
enum Event: Equatable, CustomStringConvertible {
|
||||
enum Event: CustomStringConvertible {
|
||||
case subscription(StringSubscription)
|
||||
case value(Value)
|
||||
case completion(Subscribers.Completion<Failure>)
|
||||
|
||||
static func == (lhs: Event, rhs: Event) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.value(lhs), .value(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.completion(lhs), .completion(rhs)):
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .subscription(let subscription):
|
||||
@@ -175,12 +157,69 @@ final class TrackingSubscriberBase<Value: Equatable, Failure: Error>
|
||||
return "\(type(of: self)): \(history)"
|
||||
}
|
||||
|
||||
func assertHistoryEqual(_ expected: [Event],
|
||||
valueComparator: (Value, Value) -> Bool,
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
|
||||
let equals = history.count == expected.count &&
|
||||
zip(history, expected)
|
||||
.allSatisfy { $0.isEqual(to: $1, valueComparator: valueComparator) }
|
||||
|
||||
XCTAssert(equals,
|
||||
"\(history) is not equal to \(expected)",
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
deinit {
|
||||
onDeinit?()
|
||||
_onDeinit?()
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase where Value: Equatable {
|
||||
func assertHistoryEqual(_ expected: [Event],
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line) {
|
||||
assertHistoryEqual(expected, valueComparator: ==, file: file, line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event {
|
||||
func isEqual(to other: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
valueComparator: (Value, Value) -> Bool) -> Bool {
|
||||
switch (self, other) {
|
||||
case let (.subscription(lhs), .subscription(rhs)):
|
||||
return lhs == rhs
|
||||
case let (.value(lhs), .value(rhs)):
|
||||
return valueComparator(lhs, rhs)
|
||||
case let (.completion(lhs), .completion(rhs)):
|
||||
switch (lhs, rhs) {
|
||||
case (.finished, .finished):
|
||||
return true
|
||||
case let (.failure(lhs), .failure(rhs)):
|
||||
return (lhs as? TestingError) == (rhs as? TestingError)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
extension TrackingSubscriberBase.Event: Equatable where Value: Equatable {
|
||||
|
||||
static func == (lhs: TrackingSubscriberBase<Value, Failure>.Event,
|
||||
rhs: TrackingSubscriberBase<Value, Failure>.Event) -> Bool {
|
||||
return lhs.isEqual(to: rhs, valueComparator: ==)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
typealias TrackingSubject<Output: Equatable> = TrackingSubjectBase<Output, TestingError>
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
//
|
||||
// AllSatisfyTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class AllSatisfyTests: XCTestCase {
|
||||
|
||||
// MARK: - AllSatisfy
|
||||
|
||||
func testAllSatisfyAllElementsSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.allSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyContainsElementNotSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.allSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.allSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testAllSatisfyRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.allSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testAllSatisfyReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: .earlyCompletion(false),
|
||||
{ $0.allSatisfy { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "AllSatisfy",
|
||||
expectedResult: .normalCompletion(true),
|
||||
{ $0.allSatisfy { $0 == 0 } }
|
||||
)
|
||||
}
|
||||
|
||||
func testAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.allSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
func testAllSatisfyReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "AllSatisfy",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "AllSatisfy",
|
||||
{ $0.allSatisfy(AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryAllSatisfy
|
||||
|
||||
func testTryAllSatisfyAllElementsSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyContainsElementNotSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.tryAllSatisfy(predicate) }
|
||||
)
|
||||
}
|
||||
|
||||
func testFailureBecauseOfThrow() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input < 3
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryAllSatisfy",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryAllSatisfy(predicate) })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: true,
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryAllSatisfyRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryAllSatisfyReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .earlyCompletion(false),
|
||||
{ $0.tryAllSatisfy { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .normalCompletion(true),
|
||||
{ $0.tryAllSatisfy { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryAllSatisfy",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryAllSatisfy { _ in throw TestingError.oops } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryAllSatisfyLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryAllSatisfy { _ in true } })
|
||||
}
|
||||
|
||||
func testTryAllSatisfyReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryAllSatisfy",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryAllSatisfy",
|
||||
{ $0.tryAllSatisfy(AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
/// Publishes -2, 0, 2, 4, 7
|
||||
static func testAllElementsSatisfyPredicate<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Bool,
|
||||
countPredicateCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Bool {
|
||||
|
||||
var predicateCounter = 0
|
||||
|
||||
func predicate(_ value: Int) -> Bool {
|
||||
predicateCounter += 1
|
||||
return value.isMultiple(of: 2)
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, predicate) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
}
|
||||
|
||||
/// Publishes -2, 0, 2, 4, 7, 8, 3
|
||||
static func testContainsElementNotSatisfyingPredicate<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Bool,
|
||||
countPredicateCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Bool {
|
||||
|
||||
var predicateCounter = 0
|
||||
|
||||
func predicate(_ value: Int) -> Bool {
|
||||
predicateCounter += 1
|
||||
return value.isMultiple(of: 2)
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, predicate) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(-2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 4)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none)
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 5)
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(8), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countPredicateCalls {
|
||||
XCTAssertEqual(predicateCounter, 5)
|
||||
}
|
||||
}
|
||||
|
||||
static func shouldNotBeCalled(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (Int) -> Bool {
|
||||
return { _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// CollectTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CollectTests: XCTestCase {
|
||||
|
||||
func testBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "Collect",
|
||||
expectedResult: [1, 2, 3, 4, 5],
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Collect",
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Collect",
|
||||
expectedResult: [],
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.collect() }
|
||||
}
|
||||
|
||||
func testRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.collect() }
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests
|
||||
.testReceiveSubscriptionTwice(expectedSubscription: "Collect",
|
||||
expectedResult: .normalCompletion([0]),
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.collect() })
|
||||
}
|
||||
|
||||
func testCollectReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Collect",
|
||||
customMirror: expectedChildren(("count", "0")),
|
||||
playgroundDescription: "Collect",
|
||||
{ $0.collect() })
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ final class CompactMapTests: XCTestCase {
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testTryMapFailureBecauseOfThrow() {
|
||||
func testTryCompactMapFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
@@ -79,7 +79,7 @@ final class CompactMapTests: XCTestCase {
|
||||
XCTAssertEqual(counter, 3)
|
||||
}
|
||||
|
||||
func testTryMapFailureOnCompletion() {
|
||||
func testTryCompactMapFailureOnCompletion() {
|
||||
|
||||
let publisher = PassthroughSubject<String, Error>()
|
||||
let compactMap = publisher.tryCompactMap(Int.init)
|
||||
@@ -313,61 +313,18 @@ final class CompactMapTests: XCTestCase {
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send("31")
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
func testCompactMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: "31",
|
||||
cancellingSubscriptionReleasesSubscriber: false) {
|
||||
$0.compactMap(Int.init)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
func testTryCompactMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: "31",
|
||||
cancellingSubscriptionReleasesSubscriber: false) {
|
||||
$0.tryCompactMap(Int.init)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<String, TestingError>()
|
||||
let compactMap = passthrough.compactMap(Int.init)
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
compactMap.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send("31")
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
}
|
||||
|
||||
func testCompactMapOperatorSpecializationForCompactMap() {
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
//
|
||||
// ComparisonTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ComparisonTests: XCTestCase {
|
||||
|
||||
// MARK: - Comparison
|
||||
|
||||
func testComparisonBasicBehavior() {
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 15,
|
||||
semantics: .max,
|
||||
countComparatorCalls: false,
|
||||
{ upstream, _ in upstream.max() }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
countComparatorCalls: false,
|
||||
{ upstream, _ in upstream.min() }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 8,
|
||||
semantics: .max,
|
||||
{ upstream, comparator in upstream.max(by: comparator) }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "Comparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
{ upstream, comparator in upstream.min(by: comparator) }
|
||||
)
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.max() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.min() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Comparison",
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min() })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Comparison",
|
||||
expectedResult: nil,
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.max() }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.min() }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.max(by: shouldNotBeCalled()) }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.min(by: shouldNotBeCalled()) }
|
||||
}
|
||||
|
||||
func testComparisonRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.max() }
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.min() }
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.max(by: shouldNotBeCalled())
|
||||
}
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.min(by: shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testComparisonReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.max() })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.min() })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(expectedSubscription: "Comparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.min(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.max(by: >) })
|
||||
}
|
||||
|
||||
func testComparisonReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Comparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Comparison",
|
||||
{ $0.min(by: shouldNotBeCalled()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Comparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Comparison",
|
||||
{ $0.max(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryComparison
|
||||
|
||||
func testTryComparisonBasicBehavior() {
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: 8,
|
||||
semantics: .max,
|
||||
{ upstream, comparator in upstream.tryMax(by: comparator) }
|
||||
)
|
||||
|
||||
ComparisonTests.testBasicBehavior(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: 1,
|
||||
semantics: .min,
|
||||
{ upstream, comparator in upstream.tryMin(by: comparator) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonFailureBecauseOfThrow() throws {
|
||||
|
||||
func comparator(_ lhs: Int, _ rhs: Int) throws -> Bool {
|
||||
if lhs == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return lhs < rhs
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryMax(by: comparator) })
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryComparison",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryMin(by: comparator) })
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison",
|
||||
{ $0.tryMax(by: >) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryComparison",
|
||||
{ $0.tryMin(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMax(by: >) })
|
||||
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryComparison",
|
||||
expectedResult: nil,
|
||||
{ $0.tryMin(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryMax(by: shouldNotBeCalled()) }
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryMin(by: shouldNotBeCalled()) }
|
||||
}
|
||||
|
||||
func testTryComparisonRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryMax(by: shouldNotBeCalled())
|
||||
}
|
||||
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryMin(by: shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryComparisonReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryMax(by: shouldNotBeCalled()) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryComparison",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryMin(by: shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryComparisonLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryMin(by: >) })
|
||||
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryMax(by: >) })
|
||||
}
|
||||
|
||||
func testTryComparisonReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryComparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryComparison",
|
||||
{ $0.tryMin(by: shouldNotBeCalled()) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryComparison",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryComparison",
|
||||
{ $0.tryMax(by: shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private enum ComparisonSemantics {
|
||||
case min
|
||||
case max
|
||||
}
|
||||
|
||||
private struct ComparisonHistoryElement: Equatable, CustomStringConvertible {
|
||||
let lhs: Int
|
||||
let rhs: Int
|
||||
|
||||
init(_ lhs: Int, _ rhs: Int) {
|
||||
self.lhs = lhs
|
||||
self.rhs = rhs
|
||||
}
|
||||
|
||||
var description: String { return "(\(lhs), \(rhs))" }
|
||||
}
|
||||
|
||||
/// Publishes 2, 1, 4, 6, 15, 8, `.finished`, 7, 32.
|
||||
/// Uses `Int.trailingZeroBitCount` for comparing values.
|
||||
/// Therefore, for the passed comparator 8 is max, 1 is min.
|
||||
private static func testBasicBehavior<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Int,
|
||||
semantics: ComparisonSemantics,
|
||||
countComparatorCalls: Bool = true,
|
||||
_ makeOperator: (CustomPublisher, @escaping (Int, Int) -> Bool) -> Operator
|
||||
) where Operator.Output == Int {
|
||||
|
||||
var comparisonHistory = [ComparisonHistoryElement]()
|
||||
|
||||
func comparator(_ lhs: Int, _ rhs: Int) -> Bool {
|
||||
comparisonHistory.append(.init(lhs, rhs))
|
||||
|
||||
// Some custom logic to make sure the publisher doesn't use '<'.
|
||||
return lhs.trailingZeroBitCount < rhs.trailingZeroBitCount
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(1),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { makeOperator($0, comparator) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none) // trailingZeroBitCount = 1
|
||||
XCTAssertEqual(helper.publisher.send(1), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(4), .none) // trailingZeroBitCount = 2
|
||||
XCTAssertEqual(helper.publisher.send(6), .none) // trailingZeroBitCount = 1
|
||||
XCTAssertEqual(helper.publisher.send(15), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(8), .none) // trailingZeroBitCount = 3
|
||||
XCTAssertEqual(helper.publisher.send(12), .none) // trailingZeroBitCount = 2
|
||||
|
||||
if countComparatorCalls {
|
||||
switch semantics {
|
||||
case .max:
|
||||
XCTAssertEqual(comparisonHistory, [.init(2, 1),
|
||||
.init(2, 4),
|
||||
.init(4, 6),
|
||||
.init(4, 15),
|
||||
.init(4, 8),
|
||||
.init(8, 12)])
|
||||
case .min:
|
||||
XCTAssertEqual(comparisonHistory, [.init(1, 2),
|
||||
.init(4, 1),
|
||||
.init(6, 1),
|
||||
.init(15, 1),
|
||||
.init(8, 1),
|
||||
.init(12, 1)])
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(7), .none) // trailingZeroBitCount = 0
|
||||
XCTAssertEqual(helper.publisher.send(32), .none) // trailingZeroBitCount = 5
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
|
||||
if countComparatorCalls {
|
||||
XCTAssertEqual(comparisonHistory.count, 6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldNotBeCalled(
|
||||
file: StaticString = #file, line: UInt = #line
|
||||
) -> (Int, Int) -> Bool {
|
||||
return { _, _ in
|
||||
XCTFail("Should not be called", file: file, line: line)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
//
|
||||
// ContainsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 15.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ContainsTests: XCTestCase {
|
||||
|
||||
// MARK: - Contains
|
||||
|
||||
func testContainsAllElementsNotSatisfyPredicate() {
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
countPredicateCalls: false,
|
||||
{ upstream, _ in upstream.contains(Int.max) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsContainsElementSatisfyingPredicate() {
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: true,
|
||||
countPredicateCalls: false,
|
||||
{ upstream, _ in upstream.contains(7) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Contains",
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "Contains",
|
||||
expectedResult: false,
|
||||
{ $0.contains(0) })
|
||||
}
|
||||
|
||||
func testContainsCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.contains(0) }
|
||||
}
|
||||
|
||||
func testContainsRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.contains(0) }
|
||||
}
|
||||
|
||||
func testContainsReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.contains(0) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Contains",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.contains(1) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.contains(31) })
|
||||
}
|
||||
|
||||
func testContainsReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Contains",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Contains",
|
||||
{ $0.contains(31) })
|
||||
}
|
||||
|
||||
// MARK: - ContainsWhere
|
||||
|
||||
func testContainsWhereAllElementsNotSatisfyPredicate() {
|
||||
// ContainsWhere is just the negation of AllSatisfy
|
||||
// evaluated with negated predicate
|
||||
|
||||
// "Doesn't contain an element not satisfying the predicate"
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.contains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereContainsElementSatisfyingPredicate() {
|
||||
// ContainsWhere is just the negation of AllSatisfy
|
||||
// evaluated with negated predicate
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.contains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.contains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testContainsWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.contains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testContainsWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.contains { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "ContainsWhere",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.contains { $0 > 0 } }
|
||||
)
|
||||
}
|
||||
|
||||
func testContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.contains { _ in true } })
|
||||
}
|
||||
|
||||
func testContainsWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "ContainsWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "ContainsWhere",
|
||||
{ $0.contains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
// MARK: - TryContainsWhere
|
||||
|
||||
func testTryContainsWhereAllElementsNotSatisfyPredicate() {
|
||||
// TryContainsWhere is just the negation of TryAllSatisfy
|
||||
// evaluated with negated predicate
|
||||
|
||||
// "Doesn't contain an element not satisfying the predicate"
|
||||
AllSatisfyTests.testAllElementsSatisfyPredicate(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: false,
|
||||
{ upstream, predicate in upstream.tryContains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereContainsElementSatisfyingPredicate() {
|
||||
// TryContainsWhere is just the negation of TryAllSatisfy
|
||||
// evaluated with negated predicate
|
||||
AllSatisfyTests.testContainsElementNotSatisfyingPredicate(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: true,
|
||||
{ upstream, predicate in upstream.tryContains { !predicate($0) } }
|
||||
)
|
||||
}
|
||||
|
||||
func testFailureBecauseOfThrow() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input > 3
|
||||
}
|
||||
|
||||
try ReduceTests
|
||||
.testFailureBecauseOfThrow(expectedSubscription: "TryContainsWhere",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryContains(where: predicate) })
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereUpstreamFinishesImmediately() {
|
||||
ReduceTests .testUpstreamFinishesImmediately(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: false,
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
|
||||
func testTryContainsWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled {
|
||||
$0.tryContains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryContainsWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryContains(where: AllSatisfyTests.shouldNotBeCalled())
|
||||
}
|
||||
}
|
||||
|
||||
func testTryContainsWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .earlyCompletion(true),
|
||||
{ $0.tryContains { $0 == 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .normalCompletion(false),
|
||||
{ $0.tryContains { $0 > 0 } }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryContainsWhere",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryContains { _ in throw TestingError.oops } }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryContainsWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryContains { _ in true } })
|
||||
}
|
||||
|
||||
func testTryContainsWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryContainsWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryContainsWhere",
|
||||
{ $0.tryContains(where: AllSatisfyTests.shouldNotBeCalled()) })
|
||||
}
|
||||
}
|
||||
@@ -16,126 +16,50 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class CountTests: XCTestCase {
|
||||
|
||||
func testSendsCorrectCount() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracking.history, [])
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
let sendAmount = Int.random(in: 1...1000)
|
||||
for _ in 0..<sendAmount {
|
||||
_ = publisher.send(3)
|
||||
}
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count"),
|
||||
.value(sendAmount),
|
||||
.completion(.finished)])
|
||||
func testBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "Count",
|
||||
expectedResult: 5,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testCountWaitsUntilFinishedToSend() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(42)) }
|
||||
)
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
_ = publisher.send(1)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
_ = publisher.send(2)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
_ = publisher.send(0)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count")])
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(tracking.history, [.subscription("Count"),
|
||||
.value(3),
|
||||
.completion(.finished)])
|
||||
func testUpstreamFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Count",
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testDemand() {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
$0.request(.max(42))
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in .max(4) }
|
||||
)
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
XCTAssertNotNil(downstreamSubscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(0))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(publisher.send(2), .max(0))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.request(.max(95))
|
||||
downstreamSubscription?.request(.max(5))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
|
||||
downstreamSubscription?.cancel()
|
||||
downstreamSubscription?.cancel()
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
|
||||
downstreamSubscription?.request(.max(50))
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
func testtestUpstreamFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Count",
|
||||
expectedResult: 0,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
func testAddingSubscriberRequestsUnlimitedDemand() {
|
||||
// When
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
// Given
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
countPublisher.subscribe(tracking)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.count() }
|
||||
}
|
||||
|
||||
func testReceivesSubscriptionBeforeRequestingUpstream() {
|
||||
let upstreamRequest = "Requested upstream subscription"
|
||||
let receiveDownstream = "Receive downstream"
|
||||
var receiveOrder: [String] = []
|
||||
func testRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.count() }
|
||||
}
|
||||
|
||||
let subscription = CustomSubscription(onRequest: { _ in
|
||||
receiveOrder.append(upstreamRequest)
|
||||
})
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let countPublisher = publisher.count()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { _ in
|
||||
receiveOrder.append(receiveDownstream)
|
||||
})
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests
|
||||
.testReceiveSubscriptionTwice(expectedSubscription: "Count",
|
||||
expectedResult: .normalCompletion(1),
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
countPublisher.subscribe(tracking)
|
||||
func testCountLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.count() })
|
||||
}
|
||||
|
||||
XCTAssertEqual(receiveOrder, [receiveDownstream, upstreamRequest])
|
||||
func testCountReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Count",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Count",
|
||||
{ $0.count() })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
//
|
||||
// DropTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sven Weidauer on 03.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class DropTests: XCTestCase {
|
||||
|
||||
func testDroppingTwoElements() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(42),
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.dropFirst(2) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(5), .max(3))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(3),
|
||||
.value(4),
|
||||
.value(5),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(2)),
|
||||
.requested(.max(42))])
|
||||
}
|
||||
|
||||
func testDroppingNothing() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.dropFirst(0) })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(42))])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(1))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(1))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3)])
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.value(1),
|
||||
.value(2),
|
||||
.value(3),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testDropNegativeNumberOfItemsCrash() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let drop = publisher.dropFirst(-1)
|
||||
let tracking = TrackingSubscriber()
|
||||
|
||||
assertCrashes {
|
||||
drop.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testCrashesOnZeroDemand() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.none) })
|
||||
assertCrashes {
|
||||
drop.subscribe(tracking)
|
||||
}
|
||||
}
|
||||
|
||||
func testReceiveSubscriptionTwice() throws {
|
||||
let subscription1 = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription1)
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(2)) })
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(1)),
|
||||
.requested(.max(2))])
|
||||
|
||||
let subscription2 = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription2)
|
||||
|
||||
XCTAssertEqual(subscription2.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription1)
|
||||
|
||||
XCTAssertEqual(subscription1.history, [.requested(.max(1)),
|
||||
.requested(.max(2)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.dropFirst() }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1))])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(1)), .cancelled])
|
||||
}
|
||||
|
||||
func testRequestsFromUpstreamThenSendsSubscriptionDownstream() {
|
||||
var didReceiveSubscription = false
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let drop = publisher.dropFirst()
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { _ in
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
didReceiveSubscription = true
|
||||
}
|
||||
)
|
||||
XCTAssertFalse(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(1))])
|
||||
}
|
||||
|
||||
func testReusableSubscription() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.dropFirst(3) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(312))
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(100))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(secondSubscription.history, [.requested(.max(413))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .max(3))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(secondSubscription.history, [.requested(.max(413))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Drop"),
|
||||
.completion(.finished),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testLateSubscription() throws {
|
||||
|
||||
// This publisher doesn't send a subscription when it receives a subscriber
|
||||
let publisher = CustomPublisher(subscription: nil)
|
||||
let drop = publisher.dropFirst(4)
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.max(10)) })
|
||||
|
||||
drop.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(tracking.history, [.subscription("Drop")])
|
||||
|
||||
let subscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: subscription)
|
||||
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(14))])
|
||||
XCTAssertEqual(tracking.history, [.subscription("Drop")])
|
||||
}
|
||||
|
||||
func testDropLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.dropFirst(42) })
|
||||
}
|
||||
|
||||
func testDropReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "Drop",
|
||||
customMirror: childrenIsEmpty,
|
||||
playgroundDescription: "Drop",
|
||||
{ $0.dropFirst(42) })
|
||||
}
|
||||
}
|
||||
@@ -258,59 +258,15 @@ final class DropWhileTests: XCTestCase {
|
||||
XCTAssertEqual(tracking.history, [.subscription("DropWhile")])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
func testDropWhileLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.drop(while: { _ in false }) })
|
||||
}
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let dropWhile = passthrough.drop(while: { _ in true })
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
dropWhile.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
func testTryDropWhileLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryDrop(while: { _ in false }) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,61 +230,16 @@ final class FilterTests: XCTestCase {
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
func testFilterLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.filter { _ in true } })
|
||||
}
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let filter = passthrough.filter { $0.isMultiple(of: 2) }
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
filter.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
func testTryFilterLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryFilter { _ in true } })
|
||||
}
|
||||
|
||||
func testFilterOperatorSpecializationForFilter() {
|
||||
|
||||
@@ -16,6 +16,8 @@ import OpenCombine
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class FirstTests: XCTestCase {
|
||||
|
||||
// MARK: - First
|
||||
|
||||
func testFirstDemand() throws {
|
||||
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
@@ -70,111 +72,51 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testFirstFinishesWithError() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "First") {
|
||||
$0.first()
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstFinishesImmediately() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first() })
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "First",
|
||||
expectedResult: nil) {
|
||||
$0.first()
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
func testFirstRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.first() }
|
||||
}
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("First"),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
func testFirstReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "First",
|
||||
expectedResult: .earlyCompletion(0),
|
||||
{ $0.first() }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let first = passthrough.first()
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
first.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.first() })
|
||||
}
|
||||
|
||||
func testFirstCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.first() }
|
||||
}
|
||||
|
||||
func testFirstReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "First",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "First",
|
||||
{ $0.first() })
|
||||
}
|
||||
|
||||
// MARK: - FirstWhere
|
||||
|
||||
func testFirstWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
@@ -249,113 +191,59 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesWithError() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.first(where: { $0 > 2 }) }
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryFirst") {
|
||||
$0.first(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstWhereFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryFirst",
|
||||
expectedResult: nil) {
|
||||
$0.first(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.first(where: { $0 > 0 })
|
||||
}
|
||||
}
|
||||
|
||||
func testFirstWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryFirst",
|
||||
expectedResult: .normalCompletion(nil),
|
||||
{ $0.first(where: { _ in false }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirst"),
|
||||
.completion(.failure(.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryFirst",
|
||||
expectedResult: .earlyCompletion(0),
|
||||
{ $0.first(where: { _ in true }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testFirstWhereLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
firstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var predicateDeinitCounter = 0
|
||||
let onPredicateDeinit = {
|
||||
predicateDeinitCounter += 1
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let firstWhere = passthrough.first { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
return true
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 0)
|
||||
|
||||
let subscriber =
|
||||
TrackingSubscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
firstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
}
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.first { $0 > 1 } })
|
||||
}
|
||||
|
||||
func testFirstWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.first(where: { $0 > 2 }) }
|
||||
}
|
||||
|
||||
func testFirstWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryFirst",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryFirst",
|
||||
{ $0.first(where: { $0 > 2 }) })
|
||||
}
|
||||
|
||||
// MARK: - TryFirstWhere
|
||||
|
||||
func testTryFirstWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
@@ -424,183 +312,75 @@ final class FirstTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesWithError() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.tryFirst(where: { $0 > 6 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryFirstWhere") {
|
||||
$0.tryFirst(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesWhenErrorThrown() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: {
|
||||
$0.tryFirst(where: {
|
||||
if $0 == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return $0 > 3
|
||||
})
|
||||
func testTryFirstWhereFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: nil) {
|
||||
$0.tryFirst(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryFirstWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryFirst(where: { $0 > 0 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryFirstWhereFinishesWhenErrorThrown() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input > 3
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryFirstWhere",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryFirst(where: predicate) })
|
||||
}
|
||||
|
||||
func testTryFirstWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: .normalCompletion(nil),
|
||||
{ $0.tryFirst(where: { _ in false }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: .earlyCompletion(0),
|
||||
{ $0.tryFirst(where: { _ in true }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryFirstWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryFirstWhere"),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryFirstWhere",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryFirst(where: { _ in throw TestingError.oops }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryFirstWhereLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { $0 > 1 }
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(32)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var predicateDeinitCounter = 0
|
||||
let onPredicateDeinit = {
|
||||
predicateDeinitCounter += 1
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
return true
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 0)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.max(1)) }
|
||||
)
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 1)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
}
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let tryFirstWhere = passthrough.tryFirst { _ in
|
||||
_ = TrackingSubscriber(onDeinit: onPredicateDeinit)
|
||||
throw TestingError.oops
|
||||
}
|
||||
|
||||
XCTAssertEqual(predicateDeinitCounter, 1)
|
||||
|
||||
let subscriber = TrackingSubscriberBase<Int, Error>(
|
||||
receiveSubscription: { $0.request(.max(1)) }
|
||||
)
|
||||
XCTAssertTrue(subscriber.history.isEmpty)
|
||||
tryFirstWhere.subscribe(subscriber)
|
||||
XCTAssertEqual(subscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(subscriber.inputs.count, 0)
|
||||
XCTAssertEqual(subscriber.completions.count, 1)
|
||||
XCTAssertEqual(predicateDeinitCounter, 2)
|
||||
}
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryFirst { $0 > 1 } })
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.first() })
|
||||
func testTryFirstCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryFirst(where: { $0 > 2 }) }
|
||||
}
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
func testTryFirstWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryFirstWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryFirstWhere",
|
||||
{ $0.tryFirst(where: { $0 > 2 }) })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
//
|
||||
// LastTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Joseph Spadafora on 7/9/19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class LastTests: XCTestCase {
|
||||
|
||||
// MARK: - Last
|
||||
|
||||
func testLastFinishesAndReturnsLastItem() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.last() })
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Last")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(25), .none)
|
||||
XCTAssertEqual(helper.publisher.send(42), .none)
|
||||
XCTAssertEqual(helper.publisher.send(10), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Last")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Last"),
|
||||
.value(10),
|
||||
.completion(.finished)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("Last"),
|
||||
.value(10),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testLastFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Last") {
|
||||
$0.last()
|
||||
}
|
||||
}
|
||||
|
||||
func testLastFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Last",
|
||||
expectedResult: nil) {
|
||||
$0.last()
|
||||
}
|
||||
}
|
||||
|
||||
func testLastRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.last() }
|
||||
}
|
||||
|
||||
func testLastReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Last",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.last() }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.last() })
|
||||
}
|
||||
|
||||
func testLastCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.last() }
|
||||
}
|
||||
|
||||
func testLastReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Last",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Last",
|
||||
{ $0.last() })
|
||||
}
|
||||
|
||||
// MARK: - LastWhere
|
||||
|
||||
func testLastWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.last {
|
||||
firedCounter += 1
|
||||
return $0 > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
XCTAssertEqual(firedCounter, 4)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testLastWhereFinishesAndReturnsLastMatchingItem() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.last(where: { $0 < 3 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("LastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testLastWhereFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "LastWhere") {
|
||||
$0.last(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testLastWhereFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "LastWhere",
|
||||
expectedResult: nil) {
|
||||
$0.last(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testLastWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.last(where: { $0 > 0 })
|
||||
}
|
||||
}
|
||||
|
||||
func testLastWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "LastWhere",
|
||||
expectedResult: .normalCompletion(nil),
|
||||
{ $0.last(where: { _ in false }) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "LastWhere",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.last(where: { _ in true }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testLastWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.last { $0 > 1 } })
|
||||
}
|
||||
|
||||
func testLastWhereCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.last(where: { $0 > 2 }) }
|
||||
}
|
||||
|
||||
func testLastWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "LastWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "LastWhere",
|
||||
{ $0.last(where: { $0 > 2 }) })
|
||||
}
|
||||
|
||||
// MARK: - TryLastWhere
|
||||
|
||||
func testTryLastWhereDemand() throws {
|
||||
|
||||
var firedCounter = 0
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: {
|
||||
$0.tryLast {
|
||||
firedCounter += 1
|
||||
return $0 > 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(0), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
XCTAssertEqual(firedCounter, 4)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesAndReturnsLastMatchingItem() {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(5),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: { $0.tryLast(where: { $0 < 3 }) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere")])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryLastWhere"),
|
||||
.value(2),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryLastWhere") {
|
||||
$0.tryLast(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesImmediately() {
|
||||
ReduceTests
|
||||
.testUpstreamFinishesImmediately(expectedSubscription: "TryLastWhere",
|
||||
expectedResult: nil) {
|
||||
$0.tryLast(where: { $0 > 2 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryLastWhereRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription {
|
||||
$0.tryLast(where: { $0 > 0 })
|
||||
}
|
||||
}
|
||||
|
||||
func testTryLastWhereFinishesWhenErrorThrown() throws {
|
||||
|
||||
func predicate(_ input: Int) throws -> Bool {
|
||||
if input == 3 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return input < 3
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryLastWhere",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryLast(where: predicate) })
|
||||
}
|
||||
|
||||
func testTryLastWhereReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryLastWhere",
|
||||
expectedResult: .normalCompletion(nil),
|
||||
{ $0.tryLast(where: { _ in false }) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryLastWhere",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryLast(where: { _ in true }) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryLastWhere",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryLast(where: { _ in throw TestingError.oops }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryLastWhereLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryLast { $0 > 1 } })
|
||||
}
|
||||
|
||||
func testTryLastCancelAlreadyCancelled() throws {
|
||||
try ReduceTests.testCancelAlreadyCancelled { $0.tryLast(where: { $0 > 2 }) }
|
||||
}
|
||||
|
||||
func testTryLastWhereReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryLastWhere",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryLastWhere",
|
||||
{ $0.tryLast(where: { $0 > 2 }) })
|
||||
}
|
||||
}
|
||||
@@ -188,65 +188,10 @@ final class MapErrorTests: XCTestCase {
|
||||
{ $0.mapError { $0 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let mapError = passthrough.mapError(OtherError.init)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, OtherError>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
mapError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let mapError = passthrough.mapError(OtherError.init)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, OtherError>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
mapError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let mapError = passthrough.mapError(OtherError.init)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, OtherError>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
mapError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 2)
|
||||
func testMapErrorLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true,
|
||||
{ $0.mapError(OtherError.init) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
//
|
||||
// MapKeyPathTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 08.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class MapKeyPathTests: XCTestCase {
|
||||
|
||||
func testEmpty() {
|
||||
MapTests.testEmpty(valueComparator: ==) {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
MapTests.testEmpty(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
MapTests.testEmpty(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
|
||||
func testError() {
|
||||
MapTests.testError(valueComparator: ==) {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
MapTests.testError(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
MapTests.testError(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
|
||||
func testRange() {
|
||||
MapTests.testRange(valueComparator: ==,
|
||||
mapping: { $0.doubled },
|
||||
{ $0.map(\.doubled) })
|
||||
|
||||
MapTests.testRange(valueComparator: ==,
|
||||
mapping: { ($0.doubled, $0.tripled) },
|
||||
{ $0.map(\.doubled, \.tripled) })
|
||||
|
||||
MapTests.testRange(valueComparator: ==,
|
||||
mapping: { ($0.doubled, $0.tripled, $0.quadripled) },
|
||||
{ $0.map(\.doubled, \.tripled, \.quadripled) })
|
||||
}
|
||||
|
||||
func testNoDemand() {
|
||||
MapTests.testNoDemand { $0.map(\.doubled) }
|
||||
MapTests.testNoDemand { $0.map(\.doubled, \.tripled) }
|
||||
MapTests.testNoDemand { $0.map(\.doubled, \.tripled, \.quadripled) }
|
||||
}
|
||||
|
||||
func testRequestDemandOnSubscribe() {
|
||||
MapTests.testRequestDemandOnSubscribe {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
MapTests.testRequestDemandOnSubscribe {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
MapTests.testRequestDemandOnSubscribe {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
|
||||
func testDemandOnReceive() {
|
||||
MapTests.testDemandOnReceive { $0.map(\.doubled) }
|
||||
MapTests.testDemandOnReceive { $0.map(\.doubled, \.tripled) }
|
||||
MapTests.testDemandOnReceive { $0.map(\.doubled, \.tripled, \.quadripled) }
|
||||
}
|
||||
|
||||
func testCompletion() {
|
||||
MapTests.testCompletion(valueComparator: ==) {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
MapTests.testCompletion(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
MapTests.testCompletion(valueComparator: ==) {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
|
||||
func testCancel() throws {
|
||||
try MapTests.testCancel { $0.map(\.doubled) }
|
||||
try MapTests.testCancel { $0.map(\.doubled, \.tripled) }
|
||||
try MapTests.testCancel { $0.map(\.doubled, \.tripled, \.quadripled) }
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
try MapTests.testCancelAldreadyCancelled {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
try MapTests.testCancelAldreadyCancelled {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
try MapTests.testCancelAldreadyCancelled {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
|
||||
func testMapKeyPathReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "ValueForKey",
|
||||
customMirror: expectedChildren(
|
||||
("keyPath", .contains("KeyPath"))
|
||||
),
|
||||
playgroundDescription: "ValueForKey",
|
||||
{ $0.map(\.doubled) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "ValueForKeys",
|
||||
customMirror: expectedChildren(
|
||||
("keyPath0", .contains("KeyPath")),
|
||||
("keyPath1", .contains("KeyPath"))
|
||||
),
|
||||
playgroundDescription: "ValueForKeys",
|
||||
{ $0.map(\.doubled, \.tripled) })
|
||||
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Never.self,
|
||||
description: "ValueForKeys",
|
||||
customMirror: expectedChildren(
|
||||
("keyPath0", .contains("KeyPath")),
|
||||
("keyPath1", .contains("KeyPath")),
|
||||
("keyPath2", .contains("KeyPath"))
|
||||
),
|
||||
playgroundDescription: "ValueForKeys",
|
||||
{ $0.map(\.doubled, \.tripled, \.quadripled) })
|
||||
}
|
||||
|
||||
func testMapKeyPathLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.map(\.doubled)
|
||||
}
|
||||
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.map(\.doubled, \.tripled)
|
||||
}
|
||||
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.map(\.doubled, \.tripled, \.quadripled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Int {
|
||||
fileprivate var doubled: Int { return self * 2 }
|
||||
|
||||
fileprivate var tripled: Int { return self * 3 }
|
||||
|
||||
fileprivate var quadripled: Int { return self * 4 }
|
||||
}
|
||||
@@ -17,36 +17,29 @@ import OpenCombine
|
||||
final class MapTests: XCTestCase {
|
||||
|
||||
func testEmpty() {
|
||||
MapTests.testEmpty(valueComparator: ==) {
|
||||
$0.map(String.init)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryMapEmpty() {
|
||||
// Given
|
||||
let tracking = TrackingSubscriberBase<String, TestingError>(
|
||||
let tracking = TrackingSubscriberBase<String, Error>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = TrackingSubject<Int>(
|
||||
let publisher = TrackingSubjectBase<Int, Error>(
|
||||
receiveSubscriber: {
|
||||
XCTAssertEqual(String(describing: $0), "Map")
|
||||
XCTAssertEqual(String(describing: $0), "TryMap")
|
||||
}
|
||||
)
|
||||
// When
|
||||
publisher.map(String.init).subscribe(tracking)
|
||||
publisher.tryMap(String.init).subscribe(tracking)
|
||||
// Then
|
||||
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject")])
|
||||
XCTAssertEqual(tracking.history, [.subscription("TryMap")])
|
||||
}
|
||||
|
||||
func testError() {
|
||||
// Given
|
||||
let expectedError = TestingError.oops
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
// When
|
||||
publisher.map { $0 * 2 }.subscribe(tracking)
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
// Then
|
||||
XCTAssertEqual(tracking.history, [
|
||||
.subscription("CustomSubscription"),
|
||||
.completion(.failure(expectedError)),
|
||||
.completion(.failure(expectedError))
|
||||
])
|
||||
MapTests.testError(valueComparator: ==) { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testTryMapFailureBecauseOfThrow() {
|
||||
@@ -123,106 +116,30 @@ final class MapTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testRange() {
|
||||
// Given
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
let map = publisher.map { $0 * 2 }
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
// When
|
||||
publisher.send(1)
|
||||
map.subscribe(tracking)
|
||||
publisher.send(2)
|
||||
publisher.send(3)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(5)
|
||||
// Then
|
||||
XCTAssertEqual(tracking.history, [
|
||||
.subscription("PassthroughSubject"),
|
||||
.value(4),
|
||||
.value(6),
|
||||
.completion(.finished)
|
||||
])
|
||||
let mapping: (Int) -> Int = { $0 * 2 }
|
||||
MapTests.testRange(valueComparator: ==, mapping: mapping) {
|
||||
$0.map(mapping)
|
||||
}
|
||||
}
|
||||
|
||||
func testNoDemand() {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
let tracking = TrackingSubscriber()
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
// Then
|
||||
XCTAssertTrue(subscription.history.isEmpty)
|
||||
MapTests.testNoDemand { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testDemandSubscribe() {
|
||||
// Given
|
||||
let expectedSubscribeDemand = 42
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.max(expectedSubscribeDemand)) }
|
||||
)
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.max(expectedSubscribeDemand))])
|
||||
func testRequestDemandOnSubscribe() {
|
||||
MapTests.testRequestDemandOnSubscribe { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testDemandSend() {
|
||||
var expectedReceiveValueDemand = 4
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
receiveValue: { _ in .max(expectedReceiveValueDemand) }
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(4))
|
||||
|
||||
expectedReceiveValueDemand = 120
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(120))
|
||||
func testDemandOnReceive() {
|
||||
MapTests.testDemandOnReceive { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testCompletion() {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(
|
||||
tracking.history,
|
||||
[.subscription("CustomSubscription"), .completion(.finished)]
|
||||
)
|
||||
MapTests.testCompletion(valueComparator: ==) { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testMapCancel() throws {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
})
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
publisher.send(completion: .finished)
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited), .cancelled])
|
||||
try MapTests.testCancel { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testTryMapCancel() throws {
|
||||
@@ -246,25 +163,7 @@ final class MapTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testMapCancelAlreadyCancelled() throws {
|
||||
// Given
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = publisher.map { $0 * 2 }
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriber(receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
})
|
||||
// When
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(downstreamSubscription).cancel()
|
||||
// Then
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.requested(.unlimited),
|
||||
.cancelled])
|
||||
try MapTests.testCancelAldreadyCancelled { $0.map { $0 * 2 } }
|
||||
}
|
||||
|
||||
func testTryMapCancelAlreadyCancelled() throws {
|
||||
@@ -334,61 +233,17 @@ final class MapTests: XCTestCase {
|
||||
{ $0.tryMap { $0 * 2 } })
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let map = passthrough.map { $0 * 2 }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
map.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 1)
|
||||
func testMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31, cancellingSubscriptionReleasesSubscriber: true) {
|
||||
$0.map { $0 * 2 }
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let map = passthrough.map { $0 * 2 }
|
||||
let emptySubscriber = TrackingSubscriber(onDeinit: onDeinit)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
map.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
func testTryMapLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false) {
|
||||
$0.tryMap { $0 * 2 }
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let map = passthrough.map { $0 * 2 }
|
||||
let emptySubscriber = TrackingSubscriber(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
map.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 1)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 2)
|
||||
}
|
||||
|
||||
func testMapOperatorSpecializationForMap() {
|
||||
@@ -513,4 +368,229 @@ final class MapTests: XCTestCase {
|
||||
.value(11),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
}
|
||||
|
||||
// MARK: - Generic tests (for supporting Publishers.MapKeyPath)
|
||||
|
||||
static func testEmpty<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
valueComparator: (Map.Output, Map.Output) -> Bool,
|
||||
_ map: (TrackingSubject<Int>) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = TrackingSubject<Int>()
|
||||
|
||||
map(publisher).subscribe(tracking)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("PassthroughSubject")],
|
||||
valueComparator: valueComparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testError<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
valueComparator: (Map.Output, Map.Output) -> Bool,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let expectedError = TestingError.oops
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
map(publisher).subscribe(tracking)
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("CustomSubscription"),
|
||||
.completion(.failure(expectedError)),
|
||||
.completion(.failure(expectedError))],
|
||||
valueComparator: valueComparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testRange<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
valueComparator: (Map.Output, Map.Output) -> Bool,
|
||||
mapping: (Int) -> Map.Output,
|
||||
_ map: (PassthroughSubject<Int, TestingError>) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
let map = map(publisher)
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
publisher.send(1)
|
||||
map.subscribe(tracking)
|
||||
publisher.send(2)
|
||||
publisher.send(3)
|
||||
publisher.send(completion: .finished)
|
||||
publisher.send(5)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("PassthroughSubject"),
|
||||
.value(mapping(2)),
|
||||
.value(mapping(3)),
|
||||
.completion(.finished)],
|
||||
valueComparator: valueComparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testNoDemand<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>()
|
||||
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(subscription.history.isEmpty, file: file, line: line)
|
||||
}
|
||||
|
||||
static func testRequestDemandOnSubscribe<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let expectedSubscribeDemand = 42
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.max(expectedSubscribeDemand)) }
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.max(expectedSubscribeDemand))],
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testDemandOnReceive<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
var expectedReceiveValueDemand = 4
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
receiveValue: { _ in .max(expectedReceiveValueDemand) }
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(4), file: file, line: line)
|
||||
|
||||
expectedReceiveValueDemand = 120
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(120), file: file, line: line)
|
||||
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.unlimited)],
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testCompletion<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
valueComparator: (Map.Output, Map.Output) -> Bool,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) where Map.Failure == TestingError {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.unlimited)],
|
||||
file: file,
|
||||
line: line)
|
||||
|
||||
tracking.assertHistoryEqual([.subscription("CustomSubscription"),
|
||||
.completion(.finished)],
|
||||
valueComparator: valueComparator,
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testCancel<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) throws where Map.Failure == TestingError {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
},
|
||||
receiveValue: { _ in .max(111) }
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription, file: file, line: line).cancel()
|
||||
XCTAssertEqual(publisher.send(1), .max(111), file: file, line: line)
|
||||
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.unlimited), .cancelled],
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
|
||||
static func testCancelAldreadyCancelled<Map: Publisher>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line,
|
||||
_ map: (CustomPublisher) -> Map
|
||||
) throws where Map.Failure == TestingError {
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let map = map(publisher)
|
||||
var downstreamSubscription: Subscription?
|
||||
let tracking = TrackingSubscriberBase<Map.Output, TestingError>(
|
||||
receiveSubscription: {
|
||||
$0.request(.unlimited)
|
||||
downstreamSubscription = $0
|
||||
}
|
||||
)
|
||||
|
||||
map.subscribe(tracking)
|
||||
try XCTUnwrap(downstreamSubscription, file: file, line: line).cancel()
|
||||
downstreamSubscription?.request(.unlimited)
|
||||
try XCTUnwrap(downstreamSubscription, file: file, line: line).cancel()
|
||||
|
||||
XCTAssertEqual(subscription.history,
|
||||
[.requested(.unlimited),
|
||||
.cancelled,
|
||||
.requested(.unlimited),
|
||||
.cancelled],
|
||||
file: file,
|
||||
line: line)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,6 +288,26 @@ final class MulticastTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testLazySubjectCreation() {
|
||||
let publisher = PassthroughSubject<Int, TestingError>()
|
||||
var counter = 0
|
||||
let multicast = publisher
|
||||
.multicast { () -> PassthroughSubject<Int, TestingError> in
|
||||
counter += 1
|
||||
return .init()
|
||||
}
|
||||
|
||||
multicast.subscribe(TrackingSubscriber())
|
||||
multicast.subscribe(TrackingSubscriber())
|
||||
multicast.subscribe(TrackingSubscriber())
|
||||
|
||||
XCTAssertEqual(counter, 1, "The createSubject closure should be called once")
|
||||
|
||||
_ = multicast.connect()
|
||||
|
||||
XCTAssertEqual(counter, 1, "The createSubject closure should be called once")
|
||||
}
|
||||
|
||||
func testReflection() throws {
|
||||
try MulticastTests.testGenericMulticastReflection {
|
||||
$0.multicast(PassthroughSubject.init)
|
||||
|
||||
@@ -17,15 +17,55 @@ import OpenCombine
|
||||
final class PrintTests: XCTestCase {
|
||||
|
||||
func testPrintWithoutPrefix() {
|
||||
PrintTests.testPrintWithoutPrefix(stream: HistoryStream(), stdout: false)
|
||||
}
|
||||
|
||||
func testPrintWithoutPrefixStdout() {
|
||||
let stream = HistoryStream()
|
||||
stealingStdout(to: stream) {
|
||||
PrintTests.testPrintWithoutPrefix(stream: stream, stdout: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testPrintWithPrefix() {
|
||||
PrintTests.testPrintWithPrefix(stream: HistoryStream(), stdout: false)
|
||||
}
|
||||
|
||||
func testPrintWithPrefixStdout() {
|
||||
let stream = HistoryStream()
|
||||
stealingStdout(to: stream) {
|
||||
PrintTests.testPrintWithPrefix(stream: stream, stdout: true)
|
||||
}
|
||||
}
|
||||
|
||||
func testSynchronization() {
|
||||
|
||||
let stream = HistoryStream()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: nil)
|
||||
let printer = publisher.print(to: stream)
|
||||
|
||||
let counter = Atomic(0)
|
||||
_ = printer.sink(receiveValue: { _ in counter.do { $0 += 1 } })
|
||||
|
||||
race(
|
||||
{ _ = publisher.send(12) },
|
||||
{ _ = publisher.send(34) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(counter.value, 200)
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
private static func testPrintWithoutPrefix(stream: HistoryStream, stdout: Bool) {
|
||||
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
)
|
||||
var downstreamSubscription: Subscription?
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let printer = publisher.print(to: stream)
|
||||
let printer = publisher.print(to: stdout ? nil : stream)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
stream.write("callback subscription\n")
|
||||
@@ -140,16 +180,15 @@ final class PrintTests: XCTestCase {
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testPrintWithPrefix() {
|
||||
private static func testPrintWithPrefix(stream: HistoryStream, stdout: Bool) {
|
||||
|
||||
let stream = HistoryStream()
|
||||
let subscription = CustomSubscription(
|
||||
onRequest: { _ in stream.write("callback request demand\n") },
|
||||
onCancel: { stream.write("callback cancel subscription\n") }
|
||||
)
|
||||
var downstreamSubscription: Subscription?
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let printer = publisher.print("👉", to: stream)
|
||||
let printer = publisher.print("👉", to: stdout ? nil : stream)
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: {
|
||||
stream.write("callback subscription\n")
|
||||
@@ -263,23 +302,6 @@ final class PrintTests: XCTestCase {
|
||||
|
||||
XCTAssertEqual(stream.output.value, expectedOutput)
|
||||
}
|
||||
|
||||
func testSynchronization() {
|
||||
|
||||
let stream = HistoryStream()
|
||||
let publisher = CustomPublisherBase<Int, Never>(subscription: nil)
|
||||
let printer = publisher.print(to: stream)
|
||||
|
||||
let counter = Atomic(0)
|
||||
_ = printer.sink(receiveValue: { _ in counter.do { $0 += 1 } })
|
||||
|
||||
race(
|
||||
{ _ = publisher.send(12) },
|
||||
{ _ = publisher.send(34) }
|
||||
)
|
||||
|
||||
XCTAssertEqual(counter.value, 200)
|
||||
}
|
||||
}
|
||||
|
||||
private final class HistoryStream: TextOutputStream {
|
||||
@@ -290,3 +312,18 @@ private final class HistoryStream: TextOutputStream {
|
||||
output.do { $0.append(string) }
|
||||
}
|
||||
}
|
||||
|
||||
private func stealingStdout(to stream: HistoryStream, _ body: () -> Void) {
|
||||
// See https://oleb.net/blog/2016/09/playground-print-hook/ for details
|
||||
let oldValue = _playgroundPrintHook
|
||||
_playgroundPrintHook = { string in
|
||||
stream.write("")
|
||||
if string.last == "\n" {
|
||||
// Trailing newline is actually always written separately.
|
||||
stream.write(String(string[..<string.index(before: string.endIndex)]))
|
||||
stream.write("\n")
|
||||
}
|
||||
}
|
||||
body()
|
||||
_playgroundPrintHook = oldValue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,395 @@
|
||||
//
|
||||
// ReduceTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Sergej Jaskiewicz on 10.10.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ReduceTests: XCTestCase {
|
||||
|
||||
// MARK: - Reduce
|
||||
|
||||
func testReduceBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "Reduce",
|
||||
expectedResult: 120,
|
||||
{ $0.reduce(1, *) })
|
||||
}
|
||||
|
||||
func testReduceFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "Reduce") {
|
||||
$0.reduce(1, *)
|
||||
}
|
||||
}
|
||||
|
||||
func testReduceFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "Reduce",
|
||||
expectedResult: 1) {
|
||||
$0.reduce(1, *)
|
||||
}
|
||||
}
|
||||
|
||||
func testReduceRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.reduce(0, +) }
|
||||
}
|
||||
|
||||
func testReduceReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "Reduce",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.reduce(0, +) }
|
||||
)
|
||||
}
|
||||
|
||||
func testReduceLifecycle() throws {
|
||||
try testLifecycle(sendValue: 42,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.reduce(0, +) })
|
||||
}
|
||||
|
||||
func testReduceReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Reduce",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "Reduce",
|
||||
{ $0.reduce(0, +) })
|
||||
}
|
||||
|
||||
// MARK: - TryReduce
|
||||
|
||||
func testTryReduceBasicBehavior() throws {
|
||||
try ReduceTests.testBasicReductionBehavior(expectedSubscription: "TryReduce",
|
||||
expectedResult: 120,
|
||||
{ $0.tryReduce(1, *) })
|
||||
}
|
||||
|
||||
func testTryReduceFailureBecausOfThrow() throws {
|
||||
|
||||
func reducer(_ accumulator: Int, _ newValue: Int) throws -> Int {
|
||||
if newValue == 5 {
|
||||
throw TestingError.oops
|
||||
}
|
||||
return accumulator * newValue
|
||||
}
|
||||
|
||||
try ReduceTests.testFailureBecauseOfThrow(expectedSubscription: "TryReduce",
|
||||
expectedFailure: TestingError.oops,
|
||||
{ $0.tryReduce(1, reducer) })
|
||||
}
|
||||
|
||||
func testTryReduceFinishesWithError() {
|
||||
ReduceTests.testUpstreamFinishesWithError(expectedSubscription: "TryReduce") {
|
||||
$0.tryReduce(1, *)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReduceFinishesImmediately() {
|
||||
ReduceTests.testUpstreamFinishesImmediately(expectedSubscription: "TryReduce",
|
||||
expectedResult: 1) {
|
||||
$0.tryReduce(1, *)
|
||||
}
|
||||
}
|
||||
|
||||
func testTryReduceRequestsUnlimitedThenSendsSubscription() {
|
||||
ReduceTests.testRequestsUnlimitedThenSendsSubscription { $0.tryReduce(0, +) }
|
||||
}
|
||||
|
||||
func testTryReduceReceiveSubscriptionTwice() throws {
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryReduce",
|
||||
expectedResult: .normalCompletion(0),
|
||||
{ $0.tryReduce(0, +) }
|
||||
)
|
||||
|
||||
try ReduceTests.testReceiveSubscriptionTwice(
|
||||
expectedSubscription: "TryReduce",
|
||||
expectedResult: .failure(TestingError.oops),
|
||||
{ $0.tryReduce(0, { _, _ in throw TestingError.oops }) }
|
||||
)
|
||||
}
|
||||
|
||||
func testTryReduceLifecycle() throws {
|
||||
try testLifecycle(sendValue: 42,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryReduce(0, +) })
|
||||
}
|
||||
|
||||
func testTryReduceReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryReduce",
|
||||
customMirror: reduceLikeOperatorMirror(),
|
||||
playgroundDescription: "TryReduce",
|
||||
{ $0.tryReduce(0, +) })
|
||||
}
|
||||
|
||||
// MARK: - Generic tests
|
||||
|
||||
enum PartialCompletion<Value, Failure: Error> {
|
||||
case normalCompletion(Value?)
|
||||
case earlyCompletion(Value)
|
||||
case failure(Failure)
|
||||
}
|
||||
|
||||
/// The test publishes integers from 1 to 5, then finishes.
|
||||
///
|
||||
/// This is expected to complete normally, i. e., not earlier than receiving
|
||||
/// the last value from the upstream.
|
||||
static func testBasicReductionBehavior<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Operator.Output,
|
||||
_ makeOperator: (CustomPublisherBase<Int, Never>) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Never>.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
/// The test publishes integers from 1 to 5 and expects the stream to fail.
|
||||
static func testFailureBecauseOfThrow<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedFailure: Operator.Failure,
|
||||
_ makeOperator: (CustomPublisherBase<Int, Never>) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Never>.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
helper.tracking.onFailure = { _ in
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
XCTAssertEqual(helper.publisher.send(2), .none)
|
||||
XCTAssertEqual(helper.publisher.send(3), .none)
|
||||
XCTAssertEqual(helper.publisher.send(4), .none)
|
||||
XCTAssertEqual(helper.publisher.send(5), .none)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.completion(.failure(expectedFailure))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(1))
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription),
|
||||
.completion(.failure(expectedFailure))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
static func testUpstreamFinishesWithError<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: .max(3),
|
||||
receiveValueDemand: .max(1),
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription(expectedSubscription)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(expectedSubscription),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
helper.publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(expectedSubscription),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(73), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription(expectedSubscription),
|
||||
.completion(.failure(TestingError.oops))])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
static func testUpstreamFinishesImmediately<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: Operator.Output?,
|
||||
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
|
||||
) where Operator.Output: Equatable, Operator.Failure == Error {
|
||||
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Error>.self,
|
||||
initialDemand: nil, // Downstream should receive the result nonetheless
|
||||
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]
|
||||
if let expectedResult = expectedResult {
|
||||
expectedHistory = [.subscription(expectedSubscription),
|
||||
.value(expectedResult),
|
||||
.completion(.finished)]
|
||||
} else {
|
||||
expectedHistory = [.subscription(expectedSubscription), .completion(.finished)]
|
||||
}
|
||||
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 {
|
||||
let helper = OperatorTestHelper(
|
||||
publisherType: CustomPublisherBase<Int, Error>.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: makeOperator
|
||||
)
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.unlimited)
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
}
|
||||
|
||||
static func testRequestsUnlimitedThenSendsSubscription<Operator: Publisher>(
|
||||
_ makeOperator: (CustomPublisherBase<Int, Error>) -> Operator
|
||||
) where Operator.Output: Equatable, Operator.Failure == Error {
|
||||
var didReceiveSubscription = false
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisherBase<Int, Error>(subscription: subscription)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = TrackingSubscriberBase<Operator.Output, Error>(
|
||||
receiveSubscription: { _ in
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
didReceiveSubscription = true
|
||||
}
|
||||
)
|
||||
XCTAssertFalse(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [])
|
||||
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
XCTAssertTrue(didReceiveSubscription)
|
||||
XCTAssertEqual(subscription.history, [.requested(.unlimited)])
|
||||
}
|
||||
|
||||
/// The test publishes `0`, and then finishes.
|
||||
static func testReceiveSubscriptionTwice<Operator: Publisher>(
|
||||
expectedSubscription: StringSubscription,
|
||||
expectedResult: PartialCompletion<Operator.Output, Operator.Failure>,
|
||||
_ makeOperator: (CustomPublisher) -> Operator
|
||||
) throws where Operator.Output: Equatable {
|
||||
|
||||
typealias Subscriber = TrackingSubscriberBase<Operator.Output, Operator.Failure>
|
||||
|
||||
let firstSubscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: firstSubscription)
|
||||
let operatorPublisher = makeOperator(publisher)
|
||||
let tracking = Subscriber(receiveSubscription: { $0.request(.max(1)) })
|
||||
operatorPublisher.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, [.requested(.unlimited)])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
XCTAssertEqual(tracking.history, [.subscription(expectedSubscription)])
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .none)
|
||||
publisher.send(completion: .finished)
|
||||
|
||||
let expectedSubscriberHistory: [Subscriber.Event]
|
||||
let expectedSubscriptionHistory: [CustomSubscription.Event]
|
||||
switch expectedResult {
|
||||
case .normalCompletion(nil):
|
||||
expectedSubscriberHistory = [.subscription(expectedSubscription),
|
||||
.completion(.finished)]
|
||||
expectedSubscriptionHistory = [.requested(.unlimited)]
|
||||
case let .normalCompletion(value?):
|
||||
expectedSubscriberHistory = [.subscription(expectedSubscription),
|
||||
.value(value),
|
||||
.completion(.finished)]
|
||||
expectedSubscriptionHistory = [.requested(.unlimited)]
|
||||
case let .earlyCompletion(value):
|
||||
expectedSubscriberHistory = [.subscription(expectedSubscription),
|
||||
.value(value),
|
||||
.completion(.finished)]
|
||||
expectedSubscriptionHistory = [.requested(.unlimited), .cancelled]
|
||||
case let .failure(error):
|
||||
expectedSubscriberHistory = [.subscription(expectedSubscription),
|
||||
.completion(.failure(error))]
|
||||
expectedSubscriptionHistory = [.requested(.unlimited), .cancelled]
|
||||
}
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, expectedSubscriptionHistory)
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
XCTAssertEqual(tracking.history, expectedSubscriberHistory)
|
||||
try XCTUnwrap(publisher.subscriber).receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(firstSubscription.history, expectedSubscriptionHistory)
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled, .cancelled])
|
||||
XCTAssertEqual(tracking.history, expectedSubscriberHistory)
|
||||
}
|
||||
}
|
||||
@@ -88,64 +88,10 @@ final class ReplaceErrorTests: XCTestCase {
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testLifecycle() throws {
|
||||
var deinitCounter = 0
|
||||
|
||||
let onDeinit = { deinitCounter += 1 }
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.print("test").subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
passthrough.send(completion: .failure("failure"))
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 0)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
|
||||
var subscription: Subscription?
|
||||
|
||||
do {
|
||||
let passthrough = PassthroughSubject<Int, TestingError>()
|
||||
let replaceError = passthrough.replaceError(with: 10)
|
||||
let emptySubscriber = TrackingSubscriberBase<Int, Never>(
|
||||
receiveSubscription: { subscription = $0; $0.request(.unlimited) },
|
||||
onDeinit: onDeinit
|
||||
)
|
||||
XCTAssertTrue(emptySubscriber.history.isEmpty)
|
||||
replaceError.subscribe(emptySubscriber)
|
||||
XCTAssertEqual(emptySubscriber.subscriptions.count, 1)
|
||||
passthrough.send(31)
|
||||
XCTAssertEqual(emptySubscriber.inputs.count, 1)
|
||||
XCTAssertEqual(emptySubscriber.completions.count, 0)
|
||||
XCTAssertNotNil(subscription)
|
||||
}
|
||||
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
try XCTUnwrap(subscription).cancel()
|
||||
XCTAssertEqual(deinitCounter, 0)
|
||||
func testReplaceErrorLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.replaceError(with: 10) })
|
||||
}
|
||||
|
||||
func testCancelAlreadyCancelled() throws {
|
||||
|
||||
@@ -0,0 +1,323 @@
|
||||
//
|
||||
// ScanTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Eric Patey on 27.08.2019.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
#if OPENCOMBINE_COMPATIBILITY_TEST
|
||||
import Combine
|
||||
#else
|
||||
import OpenCombine
|
||||
#endif
|
||||
|
||||
@available(macOS 10.15, iOS 13.0, *)
|
||||
final class ScanTests: XCTestCase {
|
||||
|
||||
func testDemandSend() {
|
||||
var expectedReceiveValueDemand = 4
|
||||
let subscription = CustomSubscription()
|
||||
let publisher = CustomPublisher(subscription: subscription)
|
||||
let scan = publisher.scan(0) { $0 + $1 * 2 }
|
||||
let tracking = TrackingSubscriber(
|
||||
receiveSubscription: { $0.request(.unlimited) },
|
||||
receiveValue: { _ in .max(expectedReceiveValueDemand) }
|
||||
)
|
||||
|
||||
scan.subscribe(tracking)
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(4))
|
||||
|
||||
expectedReceiveValueDemand = 120
|
||||
|
||||
XCTAssertEqual(publisher.send(0), .max(120))
|
||||
}
|
||||
|
||||
// MARK: - Scan
|
||||
|
||||
func testScanEmpty() {
|
||||
let tracking = TrackingSubscriberBase<String, TestingError>(
|
||||
receiveSubscription: { $0.request(.unlimited) }
|
||||
)
|
||||
let publisher = TrackingSubject<Int>(
|
||||
receiveSubscriber: {
|
||||
XCTAssertEqual(String(describing: $0), "Scan")
|
||||
}
|
||||
)
|
||||
|
||||
publisher.scan("", String.init(repeating:count:)).subscribe(tracking)
|
||||
// Then
|
||||
XCTAssertEqual(tracking.history, [.subscription("PassthroughSubject")])
|
||||
}
|
||||
|
||||
func testScanError() {
|
||||
let expectedError = TestingError.oops
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
|
||||
publisher.scan(666, { $0 + $1 * 2 }).subscribe(tracking)
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
publisher.send(completion: .failure(expectedError))
|
||||
|
||||
XCTAssertEqual(tracking.history, [
|
||||
.subscription("CustomSubscription"),
|
||||
.completion(.failure(expectedError)),
|
||||
.completion(.failure(expectedError))
|
||||
])
|
||||
}
|
||||
|
||||
func testScanRange() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let scan = publisher.scan(0) { $0 + $1 * 2 }
|
||||
let tracking = TrackingSubscriber(receiveSubscription: { $0.request(.unlimited) })
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
scan.subscribe(tracking)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
XCTAssertEqual(publisher.send(3), .none)
|
||||
XCTAssertEqual(publisher.send(4), .none)
|
||||
XCTAssertEqual(publisher.send(5), .none)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(6), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history, [
|
||||
.subscription("CustomSubscription"),
|
||||
.value(4),
|
||||
.value(10),
|
||||
.value(18),
|
||||
.value(28),
|
||||
.completion(.finished),
|
||||
.value(40)
|
||||
])
|
||||
}
|
||||
|
||||
func testScanImmediateCompletion() {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .max(3),
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.scan(0) { $0 + $1 * 2 } })
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.max(3))])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.completion(.finished)])
|
||||
}
|
||||
|
||||
func testScanCancel() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.scan(0) { $0 + $1 * 2 } })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("CustomSubscription"),
|
||||
.value(2),
|
||||
.completion(.finished),
|
||||
.completion(.finished),
|
||||
.completion(.failure(.oops)),
|
||||
.completion(.failure(.oops))])
|
||||
}
|
||||
|
||||
func testScanCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.scan(0, shouldNotBeCalled()) })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled,
|
||||
.requested(.max(42)),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testScanReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "Scan",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriber")),
|
||||
("result", "0")
|
||||
),
|
||||
playgroundDescription: "Scan",
|
||||
{ $0.scan(0, +) })
|
||||
}
|
||||
|
||||
func testScanLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: true,
|
||||
{ $0.scan(0, +) })
|
||||
}
|
||||
|
||||
// MARK: - TryScan
|
||||
|
||||
func testTryScanFailureOnCompletion() {
|
||||
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let scan = publisher.tryScan(0) { $0 + $1 * 2 }
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
scan.subscribe(tracking)
|
||||
publisher.send(completion: .failure(TestingError.oops))
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryScan"),
|
||||
.completion(.failure(TestingError.oops)),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testTryScanSuccess() {
|
||||
let publisher = CustomPublisher(subscription: CustomSubscription())
|
||||
let scan = publisher.tryScan(0) { $0 + $1 * 2 }
|
||||
|
||||
let tracking = TrackingSubscriberBase<Int, Error>()
|
||||
|
||||
XCTAssertEqual(publisher.send(1), .none)
|
||||
scan.subscribe(tracking)
|
||||
publisher.send(completion: .finished)
|
||||
XCTAssertEqual(publisher.send(2), .none)
|
||||
|
||||
XCTAssertEqual(tracking.history,
|
||||
[.subscription("TryScan"),
|
||||
.completion(.finished),
|
||||
.value(4)])
|
||||
}
|
||||
|
||||
func testTryScanReceiveSubscriptionTwice() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: nil,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.tryScan(0, shouldNotBeCalled()) })
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
|
||||
let secondSubscription = CustomSubscription()
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: secondSubscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [])
|
||||
XCTAssertEqual(secondSubscription.history, [.cancelled])
|
||||
|
||||
try XCTUnwrap(helper.publisher.subscriber)
|
||||
.receive(subscription: helper.subscription)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.cancelled])
|
||||
}
|
||||
|
||||
func testTryScanFailureBecauseOfThrow() {
|
||||
var counter = 0 // How many times the transform is called?
|
||||
|
||||
func reducer(_ acc: Int, _ newValue: Int) throws -> Int {
|
||||
counter += 1
|
||||
if newValue == 100 {
|
||||
throw "too much" as TestingError
|
||||
}
|
||||
return newValue * 2
|
||||
}
|
||||
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .max(3),
|
||||
createSut: { $0.tryScan(0, reducer) })
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited)])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(2), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(3), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(100), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(helper.publisher.send(9), .max(3))
|
||||
XCTAssertEqual(helper.publisher.send(100), .none)
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
helper.publisher.send(completion: .finished)
|
||||
|
||||
XCTAssertEqual(helper.tracking.history,
|
||||
[.subscription("TryScan"),
|
||||
.value(4),
|
||||
.value(6),
|
||||
.completion(.failure("too much" as TestingError)),
|
||||
.value(18)])
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
|
||||
XCTAssertEqual(counter, 5)
|
||||
}
|
||||
|
||||
func testTryScanCancel() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.tryScan(0) { $0 + $1 * 2 } })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
XCTAssertEqual(helper.publisher.send(1), .none)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .finished)
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
helper.publisher.send(completion: .failure(.oops))
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited), .cancelled])
|
||||
XCTAssertEqual(helper.tracking.history, [.subscription("TryScan"),
|
||||
.value(2)])
|
||||
}
|
||||
|
||||
func testTryScanCancelAlreadyCancelled() throws {
|
||||
let helper = OperatorTestHelper(publisherType: CustomPublisher.self,
|
||||
initialDemand: .unlimited,
|
||||
receiveValueDemand: .none,
|
||||
createSut: { $0.tryScan(0, shouldNotBeCalled()) })
|
||||
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
try XCTUnwrap(helper.downstreamSubscription).request(.max(42))
|
||||
try XCTUnwrap(helper.downstreamSubscription).cancel()
|
||||
|
||||
XCTAssertEqual(helper.subscription.history, [.requested(.unlimited),
|
||||
.cancelled])
|
||||
}
|
||||
|
||||
func testTryScanReflection() throws {
|
||||
try testReflection(parentInput: Int.self,
|
||||
parentFailure: Error.self,
|
||||
description: "TryScan",
|
||||
customMirror: expectedChildren(
|
||||
("downstream", .contains("TrackingSubscriber")),
|
||||
("status", .contains("awaitingSubscription")),
|
||||
("result", "0")
|
||||
),
|
||||
playgroundDescription: "TryScan",
|
||||
{ $0.tryScan(0, +) })
|
||||
}
|
||||
|
||||
func testTryScanLifecycle() throws {
|
||||
try testLifecycle(sendValue: 31,
|
||||
cancellingSubscriptionReleasesSubscriber: false,
|
||||
{ $0.tryScan(0, +) })
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldNotBeCalled<Accumulator, Value>(
|
||||
file: StaticString = #file,
|
||||
line: UInt = #line
|
||||
) -> (Accumulator, Value) -> Accumulator {
|
||||
return { accumulator, _ in
|
||||
XCTFail("should not be called", file: file, line: line)
|
||||
return accumulator
|
||||
}
|
||||
}
|
||||
Executable
+1269
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
def suffix_variadic(name, index, arity):
|
||||
return name + ('' if arity == 1 else str(index))
|
||||
|
||||
def list_with_suffix_variadic(name, arity):
|
||||
return [suffix_variadic(name, i, arity) for i in range(arity)]
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
find . -name '*.gyb' | \
|
||||
while read file; do \
|
||||
./utils/gyb.py \
|
||||
-Dtemplate_header="$(< utils/template_header.txt)" \
|
||||
--line-directive '' \
|
||||
-o "`dirname ${file%.gyb}`/GENERATED-`basename ${file%.gyb}`" \
|
||||
"$file"; \
|
||||
done
|
||||
@@ -0,0 +1,6 @@
|
||||
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
|
||||
// ┃ ┃
|
||||
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
|
||||
// ┃ ┃
|
||||
// ┃ ┃
|
||||
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
|
||||
Reference in New Issue
Block a user