34 Commits

Author SHA1 Message Date
Sergej Jaskiewicz 46007658a1 Add missing Equatable comformances
- Collect
- Contains
- Count
- Drop
- Last
2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz c4c7f2172d Add more tests for Scan, TryScan 2019-10-17 14:40:56 +03:00
Eric Patey 5b0a21a0b9 Implement Publishers.Scan 2019-10-17 14:40:56 +03:00
Sergej Jaskiewicz f4e191b2ff Add more tests for Publishers.Drop (#82) 2019-10-17 09:41:53 +03:00
Sven c275e51cdc Implement Publishers.Drop (#70) 2019-10-16 13:26:10 +03:00
Sergej Jaskiewicz a84105133c Increase timeout for DispatchQueueSchedulerTests.testScheduleActionOnceNow 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz ef3ebd965a Extract locking API into COpenCombineHelpers module 2019-10-16 02:10:47 +03:00
Sergej Jaskiewicz a08b99c886 Fix a compiler crash on Linux
This crash could only be reproduced in release configuration
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3398499540 Remove Unreachable.swift 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 1bf193ddaa Bump Swift version on Linux CI 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 3a88dfd76b Implemented Comparison. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bd0b69d7cb Implement Collect. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz dba76c3c41 Implement Contains, ContainsWhere, TryContainsWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 5863492753 Implement AllSatisfy, TryAllSatisfy. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Joe Spadafora 2f2e16ee1f Implement Last, LastWhere, TryLastWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz bca131c2a4 Simplify Publishers.Count. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz e999fafdce Simplify First, FirstWhere, TryFirstWhere. Use ReduceProducer. 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz b38830e0f1 Implement Reduce, TryReduce. Use ReduceProducer 2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 525405f64d Implement ReduceProducer
ReduceProducer is a helper class that makes implementing reduce-like
operators trivial.
2019-10-15 20:51:44 +03:00
Sergej Jaskiewicz 693d1145f8 Reenable compatibility tests (#79) 2019-10-13 20:22:03 +03:00
Sergej Jaskiewicz 2f9ddc2229 Bump Swift version on Travis for Linux (#78) 2019-10-10 10:52:18 +03:00
Sergej Jaskiewicz bcd1b727f8 Fix SwiftLint 2019-10-09 23:29:50 +03:00
Sergej Jaskiewicz 5d1034fcc0 Fix Linux build failure due to PTHREAD_MUTEX_ERRORCHECK there being Int, not Int32 2019-10-09 23:03:35 +03:00
Sergej Jaskiewicz 2378f3d97e Better error handling for pthread calls 2019-10-09 20:08:24 +03:00
Sergej Jaskiewicz 4a965830e7 Update docs to match Xcode 11.1 (#77) 2019-10-09 17:51:52 +03:00
Sergej Jaskiewicz 9eabadb7c9 Mention GYB in README.md 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz dcfaec2c9d Add tests for Publishers.MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 219ee38119 Update RemainingCombineInterface.swift 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 3a5389d398 GYB cleanup 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 69ead1c8fb Fix indentation 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 8e6404592e Add .gitattributes 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz 14b7ced2fe Use gyb tool to implement MapKeyPath 2019-10-09 00:12:07 +03:00
Sergej Jaskiewicz d7b9e87f6d Execute tests in parallel 2019-10-08 14:26:09 +03:00
Sergej Jaskiewicz 4fd04b8a00 Test Publishers.Print for printing to stdout 2019-10-08 14:26:09 +03:00
70 changed files with 7617 additions and 2217 deletions
+3
View File
@@ -0,0 +1,3 @@
*.swift.gyb linguist-language=Swift
**/GENERATED-* linguist-generated=true
+110
View File
@@ -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
View File
@@ -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";
+64
View File
@@ -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,
+5 -1
View File
@@ -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 -1
View File
@@ -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`.
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"
+1
View File
@@ -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)?
+5
View File
@@ -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 publishers actual type.
public func eraseToAnyPublisher() -> AnyPublisher<Output, Failure> {
return .init(self)
}
+2 -2
View File
@@ -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) {
+7 -125
View File
@@ -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()
}
}
+1 -2
View File
@@ -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 {
+4 -1
View File
@@ -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) {
+3
View File
@@ -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 closures
/// 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 closures
/// 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
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -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)]
+10
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ┃
// ┃ Auto-generated from GYB template. DO NOT EDIT! ┃
// ┃ ┃
// ┃ ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛