diff --git a/src/main/java/io/reactivex/Completable.java b/src/main/java/io/reactivex/Completable.java new file mode 100644 index 0000000000..0c2001fa85 --- /dev/null +++ b/src/main/java/io/reactivex/Completable.java @@ -0,0 +1,2251 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ +package io.reactivex; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.*; + +import org.reactivestreams.*; + +import io.reactivex.NbpObservable.NbpSubscriber; +import io.reactivex.Single.SingleSubscriber; +import io.reactivex.annotations.*; +import io.reactivex.disposables.*; +import io.reactivex.internal.disposables.*; +import io.reactivex.internal.operators.completable.*; +import io.reactivex.internal.subscriptions.DisposableSubscription; +import io.reactivex.internal.util.Exceptions; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.Schedulers; + +/** + * Represents a deferred computation without any value but only indication for completion or exception. + * + * The class follows a similar event pattern as Reactive-Streams: onSubscribe (onError|onComplete)? + */ +public class Completable { + /** + * Callback used for building deferred computations that takes a CompletableSubscriber. + */ + public interface CompletableOnSubscribe extends Consumer { + + } + + /** + * Convenience interface and callback used by the lift operator that given a child CompletableSubscriber, + * return a parent CompletableSubscriber that does any kind of lifecycle-related transformations. + */ + public interface CompletableOperator extends Function { + + } + + /** + * Represents the subscription API callbacks when subscribing to a Completable instance. + */ + public interface CompletableSubscriber { + /** + * Called once the deferred computation completes normally. + */ + void onComplete(); + + /** + * Called once if the deferred computation 'throws' an exception. + * @param e the exception, not null. + */ + void onError(Throwable e); + + /** + * Called once by the Completable to set a Disposable on this instance which + * then can be used to cancel the subscription at any time. + * @param d the Disposable instance to call dispose on for cancellation, not null + */ + void onSubscribe(Disposable d); + } + + /** + * Convenience interface and callback used by the compose operator to turn a Completable into another + * Completable fluently. + */ + public interface CompletableTransformer extends Function { + + } + + /** Single instance of a complete Completable. */ + static final Completable COMPLETE = create(s -> { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onComplete(); + }); + + /** Single instance of a never Completable. */ + static final Completable NEVER = create(s -> s.onSubscribe(EmptyDisposable.INSTANCE)); + + /** + * Returns a Completable which terminates as soon as one of the source Completables + * terminates (normally or with an error) and cancels all other Completables. + * @param sources the array of source Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable amb(Completable... sources) { + Objects.requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } + if (sources.length == 1) { + return sources[0]; + } + + return create(s -> { + CompositeDisposable set = new CompositeDisposable(); + s.onSubscribe(set); + + AtomicBoolean once = new AtomicBoolean(); + + CompletableSubscriber inner = new CompletableSubscriber() { + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + }; + + for (Completable c : sources) { + if (set.isDisposed()) { + return; + } + if (c == null) { + NullPointerException npe = new NullPointerException("One of the sources is null"); + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(npe); + } else { + RxJavaPlugins.onError(npe); + } + return; + } + if (once.get() || set.isDisposed()) { + return; + } + + // no need to have separate subscribers because inner is stateless + c.subscribe(inner); + } + }); + } + + /** + * Returns a Completable which terminates as soon as one of the source Completables + * terminates (normally or with an error) and cancels all other Completables. + * @param sources the array of source Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable amb(Iterable sources) { + Objects.requireNonNull(sources); + + return create(s -> { + CompositeDisposable set = new CompositeDisposable(); + s.onSubscribe(set); + + AtomicBoolean once = new AtomicBoolean(); + + CompletableSubscriber inner = new CompletableSubscriber() { + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onComplete(); + } + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + }; + + Iterator it; + + try { + it = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (it == null) { + s.onError(new NullPointerException("The iterator returned is null")); + return; + } + + boolean empty = true; + + for (;;) { + if (once.get() || set.isDisposed()) { + return; + } + + boolean b; + + try { + b = it.hasNext(); + } catch (Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + + if (!b) { + if (empty) { + s.onComplete(); + } + break; + } + + empty = false; + + if (once.get() || set.isDisposed()) { + return; + } + + Completable c; + + try { + c = it.next(); + } catch (Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + + if (c == null) { + NullPointerException npe = new NullPointerException("One of the sources is null"); + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(npe); + } else { + RxJavaPlugins.onError(npe); + } + return; + } + + if (once.get() || set.isDisposed()) { + return; + } + + // no need to have separate subscribers because inner is stateless + c.subscribe(inner); + } + }); + } + + /** + * Returns a Completable instance that completes immediately when subscribed to. + * @return a Completable instance that completes immediately + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable complete() { + return COMPLETE; + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable concat(Completable... sources) { + Objects.requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return sources[0]; + } + return create(new CompletableOnSubscribeConcatArray(sources)); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable concat(Iterable sources) { + Objects.requireNonNull(sources); + + return create(new CompletableOnSubscribeConcatIterable(sources)); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable concat(Observable sources) { + return concat(sources, 2); + } + + /** + * Returns a Completable which completes only when all sources complete, one after another. + * @param sources the sources to concatenate + * @param prefetch the number of sources to prefetch from the sources + * @return the Completable instance which completes only when all sources complete + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable concat(Observable sources, int prefetch) { + Objects.requireNonNull(sources); + if (prefetch < 1) { + throw new IllegalArgumentException("prefetch > 0 required but it was " + prefetch); + } + return create(new CompletableOnSubscribeConcat(sources, prefetch)); + } + + /** + * Constructs a Completable instance by wrapping the given onSubscribe callback. + * @param onSubscribe the callback which will receive the CompletableSubscriber instances + * when the Completable is subscribed to. + * @return the created Completable instance + * @throws NullPointerException if onSubscribe is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable create(CompletableOnSubscribe onSubscribe) { + Objects.requireNonNull(onSubscribe); + try { + // TODO plugin wrapping onSubscribe + + return new Completable(onSubscribe); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + RxJavaPlugins.onError(ex); + throw toNpe(ex); + } + } + + /** + * Defers the subscription to a Completable instance returned by a supplier. + * @param completableSupplier the supplier that returns the Completable that will be subscribed to. + * @return the Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable defer(Supplier completableSupplier) { + Objects.requireNonNull(completableSupplier); + return create(s -> { + Completable c; + + try { + c = completableSupplier.get(); + } catch (Throwable e) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(e); + return; + } + + if (c == null) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(new NullPointerException("The completable returned is null")); + return; + } + + c.subscribe(s); + }); + } + + /** + * Creates a Completable which calls the given error supplier for each subscriber + * and emits its returned Throwable. + *

+ * If the errorSupplier returns null, the child CompletableSubscribers will receive a + * NullPointerException. + * @param errorSupplier the error supplier, not null + * @return the new Completable instance + * @throws NullPointerException if errorSupplier is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable error(Supplier errorSupplier) { + Objects.requireNonNull(errorSupplier); + return create(s -> { + s.onSubscribe(EmptyDisposable.INSTANCE); + Throwable error; + + try { + error = errorSupplier.get(); + } catch (Throwable e) { + error = e; + } + + if (error == null) { + error = new NullPointerException("The error supplied is null"); + } + s.onError(error); + }); + } + + /** + * Creates a Completable instance that emits the given Throwable exception to subscribers. + * @param error the Throwable instance to emit, not null + * @return the new Completable instance + * @throws NullPointerException if error is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable error(Throwable error) { + Objects.requireNonNull(error); + return create(s -> { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(error); + }); + } + + /** + * Returns a Completable which when subscribed, executes the callable function, ignores its + * normal result and emits onError or onCompleted only. + * @param callable the callable instance to execute for each subscriber + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromCallable(Callable callable) { + Objects.requireNonNull(callable); + return create(s -> { + BooleanDisposable bs = new BooleanDisposable(); + s.onSubscribe(bs); + try { + callable.call(); + } catch (Throwable e) { + if (!bs.isDisposed()) { + s.onError(e); + } + return; + } + if (!bs.isDisposed()) { + s.onComplete(); + } + }); + } + + /** + * Returns a Completable instance that subscribes to the given flowable, ignores all values and + * emits only the terminal event. + * @param flowable the Flowable instance to subscribe to, not null + * @return the new Completable instance + * @throws NullPointerException if flowable is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromFlowable(Observable flowable) { + Objects.requireNonNull(flowable); + return create(cs -> { + flowable.subscribe(new Subscriber() { + + @Override + public void onComplete() { + cs.onComplete(); + } + + @Override + public void onError(Throwable t) { + cs.onError(t); + } + + @Override + public void onNext(Object t) { + // ignored + } + + @Override + public void onSubscribe(Subscription s) { + cs.onSubscribe(s::cancel); + s.request(Long.MAX_VALUE); + } + + }); + }); + } + + /** + * Returns a Completable instance that reacts to the termination of the given CompletableFuture. + *

+ * Note that cancellation from any of the subscribers to this Completable will cancel the future. + * @param future the future to react to + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromFuture(CompletableFuture future) { + Objects.requireNonNull(future); + return create(s -> { + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + s.onSubscribe(mad); + + Future f = future.whenComplete((v, e) -> { + if (!mad.isDisposed()) { + if (e != null) { + s.onError(e); + } else { + s.onComplete(); + } + } + }); + mad.set(() -> f.cancel(true)); + }); + } + + /** + * Returns a Completable instance that subscribes to the given NbpObservable, ignores all values and + * emits only the terminal event. + * @param flowable the Flowable instance to subscribe to, not null + * @return the new Completable instance + * @throws NullPointerException if flowable is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromNbpObservable(NbpObservable observable) { + Objects.requireNonNull(observable); + return create(s -> { + observable.subscribe(new NbpSubscriber() { + + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onNext(Object value) { + // ignored + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(d); + } + + }); + }); + } + + /** + * Returns a Completable instance that runs the given Runnable for each subscriber and + * emits either an unchecked exception or simply completes. + * @param run the runnable to run for each subscriber + * @return the new Completable instance + * @throws NullPointerException if run is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromRunnable(Runnable run) { + Objects.requireNonNull(run); + return create(s -> { + BooleanDisposable bs = new BooleanDisposable(); + s.onSubscribe(bs); + try { + run.run(); + } catch (Throwable e) { + if (!bs.isDisposed()) { + s.onError(e); + } + return; + } + if (!bs.isDisposed()) { + s.onComplete(); + } + }); + } + + /** + * Returns a Completable instance that when subscribed to, subscribes to the Single instance and + * emits a completion event if the single emits onSuccess or forwards any onError events. + * @param single the Single instance to subscribe to, not null + * @return the new Completable instance + * @throws NullPointerException if single is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable fromSingle(Single single) { + Objects.requireNonNull(single); + return create(s -> { + single.subscribe(new SingleSubscriber() { + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(d); + } + + @Override + public void onSuccess(Object value) { + s.onComplete(); + } + + }); + }); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable merge(Completable... sources) { + Objects.requireNonNull(sources); + if (sources.length == 0) { + return complete(); + } else + if (sources.length == 1) { + return sources[0]; + } + return create(new CompletableOnSubscribeMergeArray(sources)); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public static Completable merge(Iterable sources) { + Objects.requireNonNull(sources); + return create(new CompletableOnSubscribeMergeIterable(sources)); + } + + /** + * Returns a Completable instance that subscribes to all sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable merge(Observable sources) { + return merge0(sources, Integer.MAX_VALUE, false); + } + + /** + * Returns a Completable instance that keeps subscriptions to a limited number of sources at once and + * completes only when all source Completables complete or one of them emits an error. + * @param sources the iterable sequence of sources. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @return the new Completable instance + * @throws NullPointerException if sources is null + * @throws IllegalArgumentException if maxConcurrency is less than 1 + */ + public static Completable merge(Observable sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, false); + + } + + /** + * Returns a Completable instance that keeps subscriptions to a limited number of sources at once and + * completes only when all source Completables terminate in one way or another, combining any exceptions + * thrown by either the sources Observable or the inner Completable instances. + * @param sources the iterable sequence of sources. + * @param maxConcurrency the maximum number of concurrent subscriptions + * @param delayErrors delay all errors from the main source and from the inner Completables? + * @return the new Completable instance + * @throws NullPointerException if sources is null + * @throws IllegalArgumentException if maxConcurrency is less than 1 + */ + protected static Completable merge0(Observable sources, int maxConcurrency, boolean delayErrors) { + Objects.requireNonNull(sources); + if (maxConcurrency < 1) { + throw new IllegalArgumentException("maxConcurrency > 0 required but it was " + maxConcurrency); + } + return create(new CompletableOnSubscribeMerge(sources, maxConcurrency, delayErrors)); + } + + /** + * Returns a Completable that subscribes to all Completables in the source array and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the array of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Completable... sources) { + Objects.requireNonNull(sources); + return create(new CompletableOnSubscribeMergeDelayErrorArray(sources)); + } + + /** + * Returns a Completable that subscribes to all Completables in the source sequence and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Iterable sources) { + Objects.requireNonNull(sources); + return create(new CompletableOnSubscribeMergeDelayErrorIterable(sources)); + } + + + /** + * Returns a Completable that subscribes to all Completables in the source sequence and delays + * any error emitted by either the sources observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Observable sources) { + return merge0(sources, Integer.MAX_VALUE, true); + } + + /** + * Returns a Completable that subscribes to a limited number of inner Completables at once in + * the source sequence and delays any error emitted by either the sources + * observable or any of the inner Completables until all of + * them terminate in a way or another. + * @param sources the sequence of Completables + * @return the new Completable instance + * @throws NullPointerException if sources is null + */ + public static Completable mergeDelayError(Observable sources, int maxConcurrency) { + return merge0(sources, maxConcurrency, true); + } + + /** + * Returns a Completable that never calls onError or onComplete. + * @return the singleton instance that never calls onError or onComplete + */ + public static Completable never() { + return NEVER; + } + + /** + * Returns a Completable instance that fires its onComplete event after the given delay ellapsed. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.COMPUTATION) + public static Completable timer(long delay, TimeUnit unit) { + return timer(delay, unit, Schedulers.computation()); + } + + /** + * Returns a Completable instance that fires its onComplete event after the given delay ellapsed + * by using the supplied scheduler. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public static Completable timer(long delay, TimeUnit unit, Scheduler scheduler) { + Objects.requireNonNull(unit); + Objects.requireNonNull(scheduler); + return create(s -> { + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + s.onSubscribe(mad); + if (!mad.isDisposed()) { + mad.set(scheduler.scheduleDirect(() -> { + s.onComplete(); + }, delay, unit)); + } + }); + } + + /** + * Creates a NullPointerException instance and sets the given Throwable as its initial cause. + * @param ex the Throwable instance to use as cause, not null (not verified) + * @return the created NullPointerException + */ + static NullPointerException toNpe(Throwable ex) { + NullPointerException npe = new NullPointerException("Actually not, but can't pass out an exception otherwise..."); + npe.initCause(ex); + return npe; + } + + /** + * Returns a Completable instance which manages a resource along + * with a custom Completable instance while the subscription is active. + *

+ * This overload performs an eager unsubscription before the terminal event is emitted. + * + * @param resourceSupplier the supplier that returns a resource to be managed. + * @param completableFunction the function that given a resource returns a Completable instance that will be subscribed to + * @param disposer the consumer that disposes the resource created by the resource supplier + * @return the new Completable instance + */ + public static Completable using(Supplier resourceSupplier, + Function completableFunction, + Consumer disposer) { + return using(resourceSupplier, completableFunction, disposer, true); + } + + /** + * Returns a Completable instance which manages a resource along + * with a custom Completable instance while the subscription is active and performs eager or lazy + * resource disposition. + *

+ * If this overload performs a lazy unsubscription after the terminal event is emitted. + * Exceptions thrown at this time will be delivered to RxJavaPlugins only. + * + * @param resourceSupplier the supplier that returns a resource to be managed + * @param completableFunction the function that given a resource returns a non-null + * Completable instance that will be subscribed to + * @param disposer the consumer that disposes the resource created by the resource supplier + * @param eager if true, the resource is disposed before the terminal event is emitted, if false, the + * resource is disposed after the terminal event has been emitted + * @return the new Completable instance + */ + public static Completable using(Supplier resourceSupplier, + Function completableFunction, + Consumer disposer, + boolean eager) { + Objects.requireNonNull(resourceSupplier); + Objects.requireNonNull(completableFunction); + Objects.requireNonNull(disposer); + + return create(s -> { + R resource; + + try { + resource = resourceSupplier.get(); + } catch (Throwable e) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(e); + return; + } + + Completable cs; + + try { + cs = completableFunction.apply(resource); + } catch (Throwable e) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(e); + return; + } + + if (cs == null) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(new NullPointerException("The completable supplied is null")); + return; + } + + AtomicBoolean once = new AtomicBoolean(); + + cs.subscribe(new CompletableSubscriber() { + Disposable d; + void dispose() { + d.dispose(); + if (once.compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable ex) { + RxJavaPlugins.onError(ex); + } + } + } + + @Override + public void onComplete() { + if (eager) { + if (once.compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable ex) { + s.onError(ex); + return; + } + } + } + + s.onComplete(); + + if (!eager) { + dispose(); + } + } + + @Override + public void onError(Throwable e) { + if (eager) { + if (once.compareAndSet(false, true)) { + try { + disposer.accept(resource); + } catch (Throwable ex) { + ex.addSuppressed(e); + e = ex; + } + } + } + + s.onError(e); + + if (!eager) { + dispose(); + } + } + + @Override + public void onSubscribe(Disposable d) { + this.d = d; + s.onSubscribe(this::dispose); + } + }); + }); + } + + /** The actual subscription action. */ + private final CompletableOnSubscribe onSubscribe; + + /** + * Constructs a Completable instance with the given onSubscribe callback. + * @param onSubscribe the callback that will receive CompletableSubscribers when they subscribe, + * not null (not verified) + */ + protected Completable(CompletableOnSubscribe onSubscribe) { + this.onSubscribe = onSubscribe; + } + + /** + * Returns a Completable that emits the a terminated event of either this Completable + * or the other Completable whichever fires first. + * @param other the other Completable, not null + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable ambWith(Completable other) { + Objects.requireNonNull(other); + return amb(this, other); + } + + /** + * Subscribes to and awaits the termination of this Completable instance in a blocking manner and + * rethrows any exception emitted. + * @throws RuntimeException wrapping an InterruptedException if the current thread is interrupted + */ + @SchedulerSupport(SchedulerKind.NONE) + public final void await() { + CountDownLatch cdl = new CountDownLatch(1); + Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Disposable d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + return; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + } + + /** + * Subscribes to and awaits the termination of this Completable instance in a blocking manner + * with a specific timeout and rethrows any exception emitted within the timeout window. + * @param timeout the timeout value + * @param unit the timeout unit + * @return true if the this Completable instance completed normally within the time limit, + * false if the timeout ellapsed before this Completable terminated. + * @throws RuntimeException wrapping an InterruptedException if the current thread is interrupted + */ + @SchedulerSupport(SchedulerKind.NONE) + public final boolean await(long timeout, TimeUnit unit) { + Objects.requireNonNull(unit); + + CountDownLatch cdl = new CountDownLatch(1); + Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Disposable d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + return true; + } + boolean b; + try { + b = cdl.await(timeout, unit); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (b) { + if (err[0] != null) { + Exceptions.propagate(err[0]); + } + } + return b; + } + + /** + * Calls the given transformer function with this instance and returns the function's resulting + * Completable. + * @param transformer the transformer function, not null + * @return the Completable returned by the function + * @throws NullPointerException if transformer is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable compose(CompletableTransformer transformer) { + return to(transformer); + } + + /** + * Concatenates this Completable with another Completable. + * @param other the other Completable, not null + * @return the new Completable which subscribes to this and then the other Completable + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable concatWith(Completable other) { + Objects.requireNonNull(other); + return concat(this, other); + } + + /** + * Returns a Completable which delays the emission of the completion event by the given time. + * @param delay the delay time + * @param unit the delay unit + * @return the new Completable instance + * @throws NullPointerException if unit is null + */ + @SchedulerSupport(SchedulerKind.COMPUTATION) + public final Completable delay(long delay, TimeUnit unit) { + return delay(delay, unit, Schedulers.computation(), false); + } + + /** + * Returns a Completable which delays the emission of the completion event by the given time while + * running on the specified scheduler. + * @param delay the delay time + * @param unit the delay unit + * @param scheduler the scheduler to run the delayed completion on + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable delay(long delay, TimeUnit unit, Scheduler scheduler) { + return delay(delay, unit, scheduler, false); + } + + /** + * Returns a Completable which delays the emission of the completion event, and optionally the error as well, by the given time while + * running on the specified scheduler. + * @param delay the delay time + * @param unit the delay unit + * @param scheduler the scheduler to run the delayed completion on + * @param delayError delay the error emission as well? + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable delay(long delay, TimeUnit unit, Scheduler scheduler, boolean delayError) { + Objects.requireNonNull(unit); + Objects.requireNonNull(scheduler); + return create(s -> { + CompositeDisposable set = new CompositeDisposable(); + + subscribe(new CompletableSubscriber() { + + + @Override + public void onComplete() { + set.add(scheduler.scheduleDirect(() -> { + s.onComplete(); + }, delay, unit)); + } + + @Override + public void onError(Throwable e) { + if (delayError) { + set.add(scheduler.scheduleDirect(() -> { + s.onError(e); + }, delay, unit)); + } else { + s.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + s.onSubscribe(set); + } + + }); + }); + } + + /** + * Returns a Completable which calls the given onComplete callback if this Completable completes. + * @param onComplete the callback to call when this emits an onComplete event + * @return the new Completable instance + * @throws NullPointerException if onComplete is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable doOnComplete(Runnable onComplete) { + return doOnLifecycle(d -> { }, e -> { }, onComplete, () -> { }, () -> { }); + } + + /** + * Returns a Completable which calls the giveon onDispose callback if the child subscriber cancels + * the subscription. + * @param onDispose the callback to call when the child subscriber cancels the subscription + * @return the new Completable instance + * @throws NullPointerException if onDispose is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable doOnDispose(Runnable onDispose) { + return doOnLifecycle(d -> { }, e -> { }, () -> { }, () -> { }, onDispose); + } + + /** + * Returns a Completable which calls the given onError callback if this Completable emits an error. + * @param onError the error callback + * @return the new Completable instance + * @throws NullPointerException if onError is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable doOnError(Consumer onError) { + return doOnLifecycle(d -> { }, onError, () -> { }, () -> { }, () -> { }); + } + + /** + * Returns a Completable instance that calls the various callbacks on the specific + * lifecycle events. + * @param onSubscribe the consumer called when a CompletableSubscriber subscribes. + * @param onError the consumer called when this emits an onError event + * @param onComplete the runnable called just before when this Completable completes normally + * @param onAfterComplete the runnable called after this Completable completes normally + * @param onDisposed the runnable called when the child cancels the subscription + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + protected final Completable doOnLifecycle( + Consumer onSubscribe, + Consumer onError, + Runnable onComplete, + Runnable onAfterComplete, + Runnable onDisposed) { + Objects.requireNonNull(onSubscribe); + Objects.requireNonNull(onError); + Objects.requireNonNull(onComplete); + Objects.requireNonNull(onAfterComplete); + Objects.requireNonNull(onDisposed); + return create(s -> { + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + try { + onComplete.run(); + } catch (Throwable e) { + s.onError(e); + return; + } + + s.onComplete(); + + try { + onAfterComplete.run(); + } catch (Throwable e) { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onError(Throwable e) { + try { + onError.accept(e); + } catch (Throwable ex) { + ex.addSuppressed(e); + e = ex; + } + + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + + try { + onSubscribe.accept(d); + } catch (Throwable ex) { + d.dispose(); + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(ex); + return; + } + + s.onSubscribe(() -> { + try { + onDisposed.run(); + } catch (Throwable e) { + RxJavaPlugins.onError(e); + } + d.dispose(); + }); + } + + }); + }); + } + + /** + * Returns a Completable instance that calls the given onSubscribe callback with the disposable + * that child subscribers receive on subscription. + * @param onSubscribe the callback called when a child subscriber subscribes + * @return the new Completable instance + * @throws NullPointerException if onSubscribe is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable doOnSubscribe(Consumer onSubscribe) { + return doOnLifecycle(onSubscribe, e -> { }, () -> { }, () -> { }, () -> { }); + } + + /** + * Returns a Completable instance that calls the given onTerminate callback just before this Completable + * completes normally or with an exception + * @param onTerminate the callback to call just before this Completable terminates + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable doOnTerminate(Runnable onTerminate) { + return doOnLifecycle(s -> { }, e -> onTerminate.run(), onTerminate, () -> { }, () -> { }); + } + + /** + * Returns a completable that first runs this Completable + * and then the other completable. + *

+ * This is an alias for {@link #concatWith(Completable)}. + * @param other the other Completable, not null + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable endWith(Completable other) { + return concatWith(other); + } + /** + * Returns an NbpObservable that first runs this Completable instance and + * resumes with the given next Observable. + * @param next the next Observable to continue + * @return the new Observable instance + * @throws NullPointerException if next is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final NbpObservable endWith(NbpObservable next) { + return next.startWith(toNbpObservable()); + } + + /** + * Returns an Observable that first runs this Completable instance and + * resumes with the given next Observable. + * @param next the next Observable to continue + * @return the new Observable instance + * @throws NullPointerException if next is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Observable endWith(Observable next) { + return next.startWith(toFlowable()); + } + + /** + * Returns a Completable instace that calls the given onAfterComplete callback after this + * Completable completes normally. + * @param onAfterComplete the callback to call after this Completable emits an onComplete event. + * @return the new Completable instance + * @throws NullPointerException if onAfterComplete is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable finallyDo(Runnable onAfterComplete) { + return doOnLifecycle(d -> { }, e -> { }, () -> { }, onAfterComplete, () -> { }); + } + + /** + * Subscribes to this Completable instance and blocks until it terminates, then returns null or + * the emitted exception if any. + * @return the throwable if this terminated with an error, null otherwise + * @throws RuntimeException that wraps an InterruptedException if the wait is interrupted + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Throwable get() { + CountDownLatch cdl = new CountDownLatch(1); + Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Disposable d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + return err[0]; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + return err[0]; + } + + /** + * Subscribes to this Completable instance and blocks until it terminates or the specified timeout + * ellapses, then returns null for normal termination or the emitted exception if any. + * @return the throwable if this terminated with an error, null otherwise + * @throws RuntimeException that wraps an InterruptedException if the wait is interrupted or + * TimeoutException if the specified timeout ellapsed before it + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Throwable get(long timeout, TimeUnit unit) { + Objects.requireNonNull(unit); + + CountDownLatch cdl = new CountDownLatch(1); + Throwable[] err = new Throwable[1]; + + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err[0] = e; + cdl.countDown(); + } + + @Override + public void onSubscribe(Disposable d) { + // ignored + } + + }); + + if (cdl.getCount() == 0) { + return err[0]; + } + boolean b; + try { + b = cdl.await(timeout, unit); + } catch (InterruptedException ex) { + throw Exceptions.propagate(ex); + } + if (b) { + return err[0]; + } + Exceptions.propagate(new TimeoutException()); + return null; + } + + /** + * Lifts a CompletableSubscriber transformation into the chain of Completables. + * @param onLift the lifting function that transforms the child subscriber with a parent subscriber. + * @return the new Completable instance + * @throws NullPointerException if onLift is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable lift(CompletableOperator onLift) { + Objects.requireNonNull(onLift); + return create(s -> { + try { + // TODO plugin wrapping + + CompletableSubscriber sw = onLift.apply(s); + + subscribe(sw); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + throw toNpe(ex); + } + }); + } + + /** + * Returns a Completable which subscribes to this and the other Completable and completes + * when both of them complete or one emits an error. + * @param other the other Completable instance + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable mergeWith(Completable other) { + Objects.requireNonNull(other); + return merge(this, other); + } + + /** + * Returns a Completable which emits the terminal events from the thread of the specified scheduler. + * @param scheduler the scheduler to emit terminal events on + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable observeOn(Scheduler scheduler) { + Objects.requireNonNull(scheduler); + return create(s -> { + + ArrayCompositeResource ad = new ArrayCompositeResource<>(2, Disposable::dispose); + Scheduler.Worker w = scheduler.createWorker(); + ad.set(0, w); + + s.onSubscribe(ad); + + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + w.schedule(() -> { + try { + s.onComplete(); + } finally { + ad.dispose(); + } + }); + } + + @Override + public void onError(Throwable e) { + w.schedule(() -> { + try { + s.onError(e); + } finally { + ad.dispose(); + } + }); + } + + @Override + public void onSubscribe(Disposable d) { + ad.set(1, d); + } + + }); + }); + } + + /** + * Returns a Completable instance that if this Completable emits an error, it will emit an onComplete + * and swallow the throwable. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable onErrorComplete() { + return onErrorComplete(t -> true); + } + + /** + * Returns a Completable instance that if this Completable emits an error and the predicate returns + * true, it will emit an onComplete and swallow the throwable. + * @param predicate the predicate to call when an Throwable is emitted which should return true + * if the Throwable should be swallowed and replaced with an onComplete. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable onErrorComplete(Predicate predicate) { + Objects.requireNonNull(predicate); + + return create(s -> { + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + boolean b; + + try { + b = predicate.test(e); + } catch (Throwable ex) { + e.addSuppressed(ex); + s.onError(e); + return; + } + + if (b) { + s.onComplete(); + } else { + s.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(d); + } + + }); + }); + } + + /** + * Returns a Completable instance that when encounters an error from this Completable, calls the + * specified mapper function that returns another Completable instance for it and resumes the + * execution with it. + * @param errorMapper the mapper function that takes the error and should return a Completable as + * continuation. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable onErrorResumeNext(Function errorMapper) { + Objects.requireNonNull(errorMapper); + return create(s -> { + SerialDisposable sd = new SerialDisposable(); + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + Completable c; + + try { + c = errorMapper.apply(e); + } catch (Throwable ex) { + ex.addSuppressed(e); + s.onError(ex); + return; + } + + if (c == null) { + NullPointerException npe = new NullPointerException("The completable returned is null"); + npe.addSuppressed(e); + s.onError(npe); + return; + } + + c.subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + sd.set(d); + } + + }); + } + + @Override + public void onSubscribe(Disposable d) { + sd.set(d); + } + + }); + }); + } + + /** + * Returns a Completable that repeatedly subscribes to this Completable until cancelled. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable repeat() { + return fromFlowable(toFlowable().repeat()); + } + + /** + * Returns a Completable that subscribes repeatedly at most the given times to this Completable. + * @param times the number of times the resubscription should happen + * @return the new Completable instance + * @throws IllegalArgumentException if times is less than zero + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable repeat(long times) { + return fromFlowable(toFlowable().repeat(times)); + } + + /** + * Returns a Completable that repeatedly subscribes to this Completable so long as the given + * stop supplier returns false. + * @param stop the supplier that should return true to stop resubscribing. + * @return the new Completable instance + * @throws NullPointerException if stop is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable repeatUntil(BooleanSupplier stop) { + Objects.requireNonNull(stop); // FIXME remove once the null-check fixes are merged with Observable + return fromFlowable(toFlowable().repeatUntil(stop)); + } + + /** + * Returns a Completable instance that repeats when the Publisher returned by the handler + * emits an item or completes when this Publisher emits a completed event. + * @param handler the function that transforms the stream of values indicating the completion of + * this Completable and returns a Publisher that emits items for repeating or completes to indicate the + * repetition should stop + * @return the new Completable instance + * @throws NullPointerException if stop is null + */ + @SchedulerSupport(SchedulerKind.NONE) + /* + * FIXME the Observable type doesn't make sense here because nulls are not allowed + * FIXME add unit test once the type has been fixed + */ + public final Completable repeatWhen(Function, ? extends Publisher> handler) { + return fromFlowable(toFlowable().repeatWhen(handler)); + } + + /** + * Returns a Completable that retries this Completable as long as it emits an onError event. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable retry() { + return fromFlowable(toFlowable().retry()); + } + + /** + * Returns a Completable that retries this Completable in case of an error as long as the predicate + * returns true. + * @param predicate the predicate called when this emits an error with the repeat count and the latest exception + * and should return true to retry. + * @return the new Completable instance + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable retry(BiPredicate predicate) { + return fromFlowable(toFlowable().retry(predicate)); + } + + /** + * Returns a Completable that when this Completable emits an error, retries at most the given + * number of times before giving up and emitting the last error. + * @param times the number of times the returned Completable should retry this Completable + * @return the new Completable instance + * @throws IllegalArgumentException if times is negative + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable retry(long times) { + return fromFlowable(toFlowable().retry(times)); + } + + /** + * Returns a Completable that when this Completable emits an error, calls the given predicate with + * the latest exception to decide whether to resubscribe to this or not. + * @param predicate the predicate that is called with the latest throwable and should return + * true to indicate the returned Completable should resubscribe to this Completable. + * @return the new Completable instance + * @throws NullPointerException if predicate is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable retry(Predicate predicate) { + return fromFlowable(toFlowable().retry(predicate)); + } + + /** + * Returns a Completable which given a Publisher and when this Completable emits an error, delivers + * that error through an Observable and the Publisher should return a value indicating a retry in response + * or a terminal event indicating a termination. + * @param handler the handler that receives an Observable delivering Throwables and should return a Publisher that + * emits items to indicate retries or emits terminal events to indicate termination. + * @return the new Completable instance + * @throws NullPointerException if handler is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable retryWhen(Function, ? extends Publisher> handler) { + return fromFlowable(toFlowable().retryWhen(handler)); + } + + /** + * Returns a Completable which first runs the other Completable + * then this completable if the other completed normally. + * @param other the other completable to run first + * @return the new Completable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Completable startWith(Completable other) { + Objects.requireNonNull(other); + return concat(other, this); + } + + /** + * Returns an NbpObservable which first delivers the events + * of the other NbpObservable then runs this Completable. + * @param other the other NbpObservable to run first + * @return the new NbpObservable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final NbpObservable startWith(NbpObservable other) { + Objects.requireNonNull(other); + return other.endWith(toNbpObservable()); + } + /** + * Returns an Observable which first delivers the events + * of the other Observable then runs this Completable. + * @param other the other Observable to run first + * @return the new Observable instance + * @throws NullPointerException if other is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Observable startWith(Observable other) { + Objects.requireNonNull(other); + return other.endWith(toFlowable()); + } + + /** + * Subscribes to this Completable and returns a Disposable which can be used to cancel + * the subscription. + * @return the Disposable that allows cancelling the subscription + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Disposable subscribe() { + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + subscribe(new CompletableSubscriber() { + @Override + public void onComplete() { + // nothing to do + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + mad.set(d); + } + }); + + return mad; + } + /** + * Subscribes the given CompletableSubscriber to this Completable instance. + * @param s the CompletableSubscriber, not null + * @throws NullPointerException if s is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final void subscribe(CompletableSubscriber s) { + Objects.requireNonNull(s); + try { + // TODO plugin wrapping the subscriber + + onSubscribe.accept(s); + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + RxJavaPlugins.onError(ex); + throw toNpe(ex); + } + } + + /** + * Subscribes to this Completable and calls back either the onError or onComplete functions. + * + * @param onError the consumer that is called if this Completable emits an error + * @param onComplete the runnable that is called if the Completable completes normally + * @return the Disposable that can be used for cancelling the subscription asynchronously + * @throws NullPointerException if either callback is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Disposable subscribe(Consumer onError, Runnable onComplete) { + Objects.requireNonNull(onError); + Objects.requireNonNull(onComplete); + + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + subscribe(new CompletableSubscriber() { + @Override + public void onComplete() { + try { + onComplete.run(); + } catch (Throwable e) { + onError(e); + } + } + + @Override + public void onError(Throwable e) { + try { + onError.accept(e); + } catch (Throwable ex) { + e.addSuppressed(ex); + RxJavaPlugins.onError(e); + } + } + + @Override + public void onSubscribe(Disposable d) { + mad.set(d); + } + }); + + return mad; + } + + /** + * Subscribes a non-backpressure NbpSubscriberto this Completable instance which + * will receive only an onError or onComplete event. + * @param s the NbpSubscriber instance, not null + * @throw NullPointerException if s is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final void subscribe(NbpSubscriber s) { + Objects.requireNonNull(s); + try { + // TODO plugin wrapping the subscriber + + subscribe(new CompletableSubscriber() { + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(d); + } + }); + + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + RxJavaPlugins.onError(ex); + throw toNpe(ex); + } + } + + /** + * Subscribes to this Completable and calls the given Runnable when this Completable + * completes normally. + *

+ * If this Completable emits an error, it is sent to RxJavaPlugins.onError and gets swallowed. + * @param onComplete the runnable called when this Completable completes normally + * @return the Disposable that allows cancelling the subscription + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Disposable subscribe(Runnable onComplete) { + Objects.requireNonNull(onComplete); + + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + subscribe(new CompletableSubscriber() { + @Override + public void onComplete() { + try { + onComplete.run(); + } catch (Throwable e) { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + mad.set(d); + } + }); + + return mad; + } + + /** + * Subscribes a reactive-streams Subscriber to this Completable instance which + * will receive only an onError or onComplete event. + * @param s the reactive-streams Subscriber, not null + * @throws NullPointerException if s is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final void subscribe(Subscriber s) { + Objects.requireNonNull(s); + try { + Subscriber sw = RxJavaPlugins.onSubscribe(s); + + if (sw == null) { + throw new NullPointerException("The RxJavaPlugins.onSubscribe returned a null Subscriber"); + } + + subscribe(new CompletableSubscriber() { + @Override + public void onComplete() { + sw.onComplete(); + } + + @Override + public void onError(Throwable e) { + sw.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + DisposableSubscription ds = new DisposableSubscription(d); + sw.onSubscribe(ds); + } + }); + + } catch (NullPointerException ex) { + throw ex; + } catch (Throwable ex) { + RxJavaPlugins.onError(ex); + throw toNpe(ex); + } + } + + /** + * Returns a Completable which subscribes the child subscriber on the specified scheduler, making + * sure the subscription side-effects happen on that specific thread of the scheduler. + * @param scheduler the Scheduler to subscribe on + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable subscribeOn(Scheduler scheduler) { + Objects.requireNonNull(scheduler); + + return create(s -> { + // FIXME cancellation of this schedule + scheduler.scheduleDirect(() -> { + subscribe(s); + }); + }); + } + + /** + * Returns a Completable that runs this Completable and emits a TimeoutException in case + * this Completable doesn't complete within the given time. + * @param timeout the timeout value + * @param unit the timeout unit + * @return the new Completable instance + * @throws NullPointerException if unit is null + */ + @SchedulerSupport(SchedulerKind.COMPUTATION) + public final Completable timeout(long timeout, TimeUnit unit) { + return timeout0(timeout, unit, Schedulers.computation(), null); + } + + /** + * Returns a Completable that runs this Completable and switches to the other Completable + * in case this Completable doesn't complete within the given time. + * @param timeout the timeout value + * @param unit the timeout unit + * @param other the other Completable instance to switch to in case of a timeout + * @return the new Completable instance + * @throws NullPointerException if unit or other is null + */ + @SchedulerSupport(SchedulerKind.COMPUTATION) + public final Completable timeout(long timeout, TimeUnit unit, Completable other) { + Objects.requireNonNull(other); + return timeout0(timeout, unit, Schedulers.computation(), other); + } + + /** + * Returns a Completable that runs this Completable and emits a TimeoutException in case + * this Completable doesn't complete within the given time while "waiting" on the specified + * Scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable timeout(long timeout, TimeUnit unit, Scheduler scheduler) { + return timeout0(timeout, unit, scheduler, null); + } + + /** + * Returns a Completable that runs this Completable and switches to the other Completable + * in case this Completable doesn't complete within the given time while "waiting" on + * the specified scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @param other the other Completable instance to switch to in case of a timeout + * @return the new Completable instance + * @throws NullPointerException if unit, scheduler or other is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable timeout(long timeout, TimeUnit unit, Scheduler scheduler, Completable other) { + Objects.requireNonNull(other); + return timeout0(timeout, unit, scheduler, other); + } + + /** + * Returns a Completable that runs this Completable and optionally switches to the other Completable + * in case this Completable doesn't complete within the given time while "waiting" on + * the specified scheduler. + * @param timeout the timeout value + * @param unit the timeout unit + * @param scheduler the scheduler to use to wait for completion + * @param other the other Completable instance to switch to in case of a timeout, + * if null a TimeoutException is emitted instead + * @return the new Completable instance + * @throws NullPointerException if unit or scheduler + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable timeout0(long timeout, TimeUnit unit, Scheduler scheduler, Completable other) { + Objects.requireNonNull(unit); + Objects.requireNonNull(scheduler); + return create(new CompletableOnSubscribeTimeout(this, timeout, unit, scheduler, other)); + } + + /** + * Allows fluent conversion to another type via a function callback. + * @param converter the function called with this which should return some other value. + * @return the converted value + * @throws NullPointerException if converter is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final U to(Function converter) { + return converter.apply(this); + } + + /** + * Returns an Observable which when subscribed to subscribes to this Completable and + * relays the terminal events to the subscriber. + * @return the new Observable created + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Observable toFlowable() { + return Observable.create(s -> { + subscribe(s); + }); + } + + /** + * Returns an NbpObservable which when subscribed to subscribes to this Completable and + * relays the terminal events to the subscriber. + * @return the new NbpObservable created + */ + @SchedulerSupport(SchedulerKind.NONE) + public final NbpObservable toNbpObservable() { + return NbpObservable.create(s -> { + subscribe(s); + }); + } + + /** + * Convers this Completable into a Single which when this Completable completes normally, + * calls the given supplier and emits its returned value through onSuccess. + * @param completionValueSupplier the value supplier called when this Completable completes normally + * @return the new Single instance + * @throws NullPointerException if completionValueSupplier is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Single toSingle(Supplier completionValueSupplier) { + Objects.requireNonNull(completionValueSupplier); + return Single.create(s -> { + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + T v; + + try { + v = completionValueSupplier.get(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (v == null) { + s.onError(new NullPointerException("The value supplied is null")); + } else { + s.onSuccess(v); + } + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(d); + } + + }); + }); + } + + /** + * Convers this Completable into a Single which when this Completable completes normally, + * emits the given value through onSuccess. + * @param completionValue the value to emit when this Completable completes normally + * @return the new Single instance + * @throws NullPointerException if completionValue is null + */ + @SchedulerSupport(SchedulerKind.NONE) + public final Single toSingleDefault(T completionValue) { + Objects.requireNonNull(completionValue); + return toSingle(() -> completionValue); + } + + /** + * Returns a Completable which makes sure when a subscriber cancels the subscription, the + * dispose is called on the specified scheduler + * @param scheduler the target scheduler where to execute the cancellation + * @return the new Completable instance + * @throws NullPointerException if scheduler is null + */ + @SchedulerSupport(SchedulerKind.CUSTOM) + public final Completable unsubscribeOn(Scheduler scheduler) { + Objects.requireNonNull(scheduler); + return create(s -> { + subscribe(new CompletableSubscriber() { + + @Override + public void onComplete() { + s.onComplete(); + } + + @Override + public void onError(Throwable e) { + s.onError(e); + } + + @Override + public void onSubscribe(Disposable d) { + s.onSubscribe(() -> { + scheduler.scheduleDirect(() -> { + d.dispose(); + }); + }); + } + + }); + }); + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcat.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcat.java new file mode 100644 index 0000000000..c4ca4e0cd2 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcat.java @@ -0,0 +1,168 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.*; +import io.reactivex.Completable.*; +import io.reactivex.disposables.Disposable; +import io.reactivex.exceptions.MissingBackpressureException; +import io.reactivex.internal.disposables.SerialResource; +import io.reactivex.internal.queue.SpscArrayQueue; +import io.reactivex.internal.subscriptions.SubscriptionHelper; +import io.reactivex.plugins.RxJavaPlugins; + +public final class CompletableOnSubscribeConcat implements CompletableOnSubscribe { + final Observable sources; + final int prefetch; + + public CompletableOnSubscribeConcat(Observable sources, int prefetch) { + this.sources = sources; + this.prefetch = prefetch; + } + + @Override + public void accept(CompletableSubscriber s) { + CompletableConcatSubscriber parent = new CompletableConcatSubscriber(s, prefetch); + sources.subscribe(parent); + } + + static final class CompletableConcatSubscriber + extends AtomicInteger + implements Subscriber, Disposable { + /** */ + private static final long serialVersionUID = 7412667182931235013L; + final CompletableSubscriber actual; + final int prefetch; + final SerialResource sr; + + final SpscArrayQueue queue; + + Subscription s; + + volatile boolean done; + + volatile int once; + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(CompletableConcatSubscriber.class, "once"); + + final ConcatInnerSubscriber inner; + + public CompletableConcatSubscriber(CompletableSubscriber actual, int prefetch) { + this.actual = actual; + this.prefetch = prefetch; + this.queue = new SpscArrayQueue<>(prefetch); + this.sr = new SerialResource<>(Disposable::dispose); + this.inner = new ConcatInnerSubscriber(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validateSubscription(this.s, s)) { + return; + } + this.s = s; + actual.onSubscribe(this); + s.request(prefetch); + } + + @Override + public void onNext(Completable t) { + if (!queue.offer(t)) { + onError(new MissingBackpressureException()); + return; + } + if (getAndIncrement() == 0) { + next(); + } + } + + @Override + public void onError(Throwable t) { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(t); + return; + } + RxJavaPlugins.onError(t); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + if (getAndIncrement() == 0) { + next(); + } + } + + void innerError(Throwable e) { + s.cancel(); + onError(e); + } + + void innerComplete() { + if (decrementAndGet() != 0) { + next(); + } + if (!done) { + s.request(1); + } + } + + @Override + public void dispose() { + s.cancel(); + sr.dispose(); + } + + void next() { + boolean d = done; + Completable c = queue.poll(); + if (c == null) { + if (d) { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onComplete(); + } + return; + } + RxJavaPlugins.onError(new IllegalStateException("Queue is empty?!")); + return; + } + + c.subscribe(inner); + } + + final class ConcatInnerSubscriber implements CompletableSubscriber { + @Override + public void onSubscribe(Disposable d) { + sr.set(d); + } + + @Override + public void onError(Throwable e) { + innerError(e); + } + + @Override + public void onComplete() { + innerComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatArray.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatArray.java new file mode 100644 index 0000000000..f6a10ba613 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatArray.java @@ -0,0 +1,93 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; + +public final class CompletableOnSubscribeConcatArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeConcatArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + ConcatInnerSubscriber inner = new ConcatInnerSubscriber(s, sources); + s.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerSubscriber extends AtomicInteger implements CompletableSubscriber { + /** */ + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableSubscriber actual; + final Completable[] sources; + + int index; + + final SerialDisposable sd; + + public ConcatInnerSubscriber(CompletableSubscriber actual, Completable[] sources) { + this.actual = actual; + this.sources = sources; + this.sd = new SerialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + sd.set(d); + } + + @Override + public void onError(Throwable e) { + actual.onError(e); + } + + @Override + public void onComplete() { + next(); + } + + void next() { + if (sd.isDisposed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + Completable[] a = sources; + do { + if (sd.isDisposed()) { + return; + } + + int idx = index++; + if (idx == a.length) { + actual.onComplete(); + return; + } + + a[idx].subscribe(this); + } while (decrementAndGet() != 0); + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatIterable.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatIterable.java new file mode 100644 index 0000000000..1d048c590b --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeConcatIterable.java @@ -0,0 +1,133 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.Iterator; +import java.util.concurrent.atomic.AtomicInteger; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; +import io.reactivex.internal.disposables.EmptyDisposable; + +public final class CompletableOnSubscribeConcatIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeConcatIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + + Iterator it; + + try { + it = sources.iterator(); + } catch (Throwable e) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(e); + return; + } + + if (it == null) { + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(new NullPointerException("The iterator returned is null")); + return; + } + + ConcatInnerSubscriber inner = new ConcatInnerSubscriber(s, it); + s.onSubscribe(inner.sd); + inner.next(); + } + + static final class ConcatInnerSubscriber extends AtomicInteger implements CompletableSubscriber { + /** */ + private static final long serialVersionUID = -7965400327305809232L; + + final CompletableSubscriber actual; + final Iterator sources; + + int index; + + final SerialDisposable sd; + + public ConcatInnerSubscriber(CompletableSubscriber actual, Iterator sources) { + this.actual = actual; + this.sources = sources; + this.sd = new SerialDisposable(); + } + + @Override + public void onSubscribe(Disposable d) { + sd.set(d); + } + + @Override + public void onError(Throwable e) { + actual.onError(e); + } + + @Override + public void onComplete() { + next(); + } + + void next() { + if (sd.isDisposed()) { + return; + } + + if (getAndIncrement() != 0) { + return; + } + + Iterator a = sources; + do { + if (sd.isDisposed()) { + return; + } + + boolean b; + try { + b = a.hasNext(); + } catch (Throwable ex) { + actual.onError(ex); + return; + } + + if (!b) { + actual.onComplete(); + return; + } + + Completable c; + + try { + c = a.next(); + } catch (Throwable ex) { + actual.onError(ex); + return; + } + + if (c == null) { + actual.onError(new NullPointerException("The completable returned is null")); + return; + } + + c.subscribe(this); + } while (decrementAndGet() != 0); + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMerge.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMerge.java new file mode 100644 index 0000000000..7a747e554c --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMerge.java @@ -0,0 +1,235 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; + +import io.reactivex.*; +import io.reactivex.Completable.*; +import io.reactivex.disposables.Disposable; +import io.reactivex.internal.disposables.SetCompositeResource; +import io.reactivex.internal.subscriptions.SubscriptionHelper; +import io.reactivex.plugins.RxJavaPlugins; + +public final class CompletableOnSubscribeMerge implements CompletableOnSubscribe { + final Observable source; + final int maxConcurrency; + final boolean delayErrors; + + public CompletableOnSubscribeMerge(Observable source, int maxConcurrency, boolean delayErrors) { + this.source = source; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + } + + @Override + public void accept(CompletableSubscriber s) { + CompletableMergeSubscriber parent = new CompletableMergeSubscriber(s, maxConcurrency, delayErrors); + source.subscribe(parent); + } + + static final class CompletableMergeSubscriber + extends AtomicInteger + implements Subscriber, Disposable { + /** */ + private static final long serialVersionUID = -2108443387387077490L; + + final CompletableSubscriber actual; + final SetCompositeResource set; + final int maxConcurrency; + final boolean delayErrors; + + Subscription s; + + volatile boolean done; + + volatile Queue errors; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ERRORS = + AtomicReferenceFieldUpdater.newUpdater(CompletableMergeSubscriber.class, Queue.class, "errors"); + + volatile int once; + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(CompletableMergeSubscriber.class, "once"); + + public CompletableMergeSubscriber(CompletableSubscriber actual, int maxConcurrency, boolean delayErrors) { + this.actual = actual; + this.maxConcurrency = maxConcurrency; + this.delayErrors = delayErrors; + this.set = new SetCompositeResource<>(Disposable::dispose); + lazySet(1); + } + + @Override + public void dispose() { + s.cancel(); + set.dispose(); + } + + @Override + public void onSubscribe(Subscription s) { + if (SubscriptionHelper.validateSubscription(this.s, s)) { + return; + } + this.s = s; + set.add(s::cancel); + actual.onSubscribe(this); + if (maxConcurrency == Integer.MAX_VALUE) { + s.request(Long.MAX_VALUE); + } else { + s.request(maxConcurrency); + } + } + + Queue getOrCreateErrors() { + Queue q = errors; + + if (q != null) { + return q; + } + + q = new ConcurrentLinkedQueue<>(); + if (ERRORS.compareAndSet(this, null, q)) { + return q; + } + return errors; + } + + @Override + public void onNext(Completable t) { + if (done) { + return; + } + + getAndIncrement(); + + t.subscribe(new CompletableSubscriber() { + Disposable d; + boolean innerDone; + @Override + public void onSubscribe(Disposable d) { + this.d = d; + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (innerDone) { + RxJavaPlugins.onError(e); + return; + } + innerDone = true; + set.remove(d); + + getOrCreateErrors().offer(e); + + terminate(); + + if (delayErrors && !done) { + s.request(1); + } + } + + @Override + public void onComplete() { + if (innerDone) { + return; + } + innerDone = true; + set.remove(d); + + terminate(); + + if (!done) { + s.request(1); + } + } + }); + } + + @Override + public void onError(Throwable t) { + if (done) { + RxJavaPlugins.onError(t); + return; + } + getOrCreateErrors().offer(t); + done = true; + terminate(); + } + + @Override + public void onComplete() { + if (done) { + return; + } + done = true; + terminate(); + } + + void terminate() { + if (decrementAndGet() == 0) { + Queue q = errors; + if (q == null || q.isEmpty()) { + actual.onComplete(); + } else { + Throwable e = collectErrors(q); + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + } else + if (!delayErrors) { + Queue q = errors; + if (q != null && !q.isEmpty()) { + Throwable e = collectErrors(q); + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + } + } + } + + /** + * Collects the Throwables from the queue, adding subsequent Throwables as suppressed to + * the first Throwable and returns it. + * @param q the queue to drain + * @return the Throwable containing all other Throwables as suppressed + */ + public static Throwable collectErrors(Queue q) { + Throwable ex = null; + + Throwable t; + int count = 0; + while ((t = q.poll()) != null) { + if (count == 0) { + ex = t; + } else { + ex.addSuppressed(t); + } + + count++; + } + return ex; + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeArray.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeArray.java new file mode 100644 index 0000000000..52d70d25f7 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeArray.java @@ -0,0 +1,88 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.concurrent.atomic.*; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; +import io.reactivex.plugins.RxJavaPlugins; + +public final class CompletableOnSubscribeMergeArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeMergeArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + CompositeDisposable set = new CompositeDisposable(); + AtomicInteger wip = new AtomicInteger(sources.length + 1); + AtomicBoolean once = new AtomicBoolean(); + + s.onSubscribe(set); + + for (Completable c : sources) { + if (set.isDisposed()) { + return; + } + + if (c == null) { + set.dispose(); + NullPointerException npe = new NullPointerException("A completable source is null"); + if (once.compareAndSet(false, true)) { + s.onError(npe); + return; + } else { + RxJavaPlugins.onError(npe); + } + } + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onComplete(); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorArray.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorArray.java new file mode 100644 index 0000000000..202a4760d0 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorArray.java @@ -0,0 +1,90 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.*; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; + +public final class CompletableOnSubscribeMergeDelayErrorArray implements CompletableOnSubscribe { + final Completable[] sources; + + public CompletableOnSubscribeMergeDelayErrorArray(Completable[] sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + CompositeDisposable set = new CompositeDisposable(); + AtomicInteger wip = new AtomicInteger(sources.length + 1); + + Queue q = new ConcurrentLinkedQueue<>(); + + s.onSubscribe(set); + + for (Completable c : sources) { + if (set.isDisposed()) { + return; + } + + if (c == null) { + q.offer(new NullPointerException("A completable source is null")); + wip.decrementAndGet(); + continue; + } + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + q.offer(e); + tryTerminate(); + } + + @Override + public void onComplete() { + tryTerminate(); + } + + void tryTerminate() { + if (wip.decrementAndGet() == 0) { + if (q.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(q)); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (q.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(q)); + } + } + + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorIterable.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorIterable.java new file mode 100644 index 0000000000..c006527ef5 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeDelayErrorIterable.java @@ -0,0 +1,154 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.*; +import java.util.concurrent.atomic.*; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; +import io.reactivex.internal.queue.MpscLinkedQueue; + +public final class CompletableOnSubscribeMergeDelayErrorIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeMergeDelayErrorIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + CompositeDisposable set = new CompositeDisposable(); + AtomicInteger wip = new AtomicInteger(1); + + Queue queue = new MpscLinkedQueue<>(); + + s.onSubscribe(set); + + Iterator iterator; + + try { + iterator = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (iterator == null) { + s.onError(new NullPointerException("The source iterator returned is null")); + return; + } + + for (;;) { + if (set.isDisposed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + if (!b) { + break; + } + + if (set.isDisposed()) { + return; + } + + Completable c; + + try { + c = iterator.next(); + } catch (Throwable e) { + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + if (set.isDisposed()) { + return; + } + + if (c == null) { + NullPointerException e = new NullPointerException("A completable source is null"); + queue.offer(e); + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + return; + } + + wip.getAndIncrement(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + queue.offer(e); + tryTerminate(); + } + + @Override + public void onComplete() { + tryTerminate(); + } + + void tryTerminate() { + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + } + }); + } + + if (wip.decrementAndGet() == 0) { + if (queue.isEmpty()) { + s.onComplete(); + } else { + s.onError(CompletableOnSubscribeMerge.collectErrors(queue)); + } + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeIterable.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeIterable.java new file mode 100644 index 0000000000..0c6947a09d --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeMergeIterable.java @@ -0,0 +1,144 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.Iterator; +import java.util.concurrent.atomic.*; + +import io.reactivex.Completable; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; +import io.reactivex.plugins.RxJavaPlugins; + +public final class CompletableOnSubscribeMergeIterable implements CompletableOnSubscribe { + final Iterable sources; + + public CompletableOnSubscribeMergeIterable(Iterable sources) { + this.sources = sources; + } + + @Override + public void accept(CompletableSubscriber s) { + CompositeDisposable set = new CompositeDisposable(); + AtomicInteger wip = new AtomicInteger(1); + AtomicBoolean once = new AtomicBoolean(); + + s.onSubscribe(set); + + Iterator iterator; + + try { + iterator = sources.iterator(); + } catch (Throwable e) { + s.onError(e); + return; + } + + if (iterator == null) { + s.onError(new NullPointerException("The source iterator returned is null")); + return; + } + + for (;;) { + if (set.isDisposed()) { + return; + } + + boolean b; + try { + b = iterator.hasNext(); + } catch (Throwable e) { + set.dispose(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + + if (!b) { + break; + } + + if (set.isDisposed()) { + return; + } + + Completable c; + + try { + c = iterator.next(); + } catch (Throwable e) { + set.dispose(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + return; + } + + if (set.isDisposed()) { + return; + } + + if (c == null) { + set.dispose(); + NullPointerException npe = new NullPointerException("A completable source is null"); + if (once.compareAndSet(false, true)) { + s.onError(npe); + } else { + RxJavaPlugins.onError(npe); + } + return; + } + + wip.getAndIncrement(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + if (once.compareAndSet(false, true)) { + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onComplete(); + } + } + } + + }); + } + + if (wip.decrementAndGet() == 0) { + if (once.compareAndSet(false, true)) { + s.onComplete(); + } + } + } +} diff --git a/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeTimeout.java b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeTimeout.java new file mode 100644 index 0000000000..90407f3075 --- /dev/null +++ b/src/main/java/io/reactivex/internal/operators/completable/CompletableOnSubscribeTimeout.java @@ -0,0 +1,107 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.operators.completable; + +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; + +import io.reactivex.*; +import io.reactivex.Completable.*; +import io.reactivex.disposables.*; +import io.reactivex.plugins.RxJavaPlugins; + +public final class CompletableOnSubscribeTimeout implements CompletableOnSubscribe { + + final Completable source; + final long timeout; + final TimeUnit unit; + final Scheduler scheduler; + final Completable other; + + public CompletableOnSubscribeTimeout(Completable source, long timeout, + TimeUnit unit, Scheduler scheduler, Completable other) { + this.source = source; + this.timeout = timeout; + this.unit = unit; + this.scheduler = scheduler; + this.other = other; + } + + @Override + public void accept(CompletableSubscriber s) { + CompositeDisposable set = new CompositeDisposable(); + s.onSubscribe(set); + + AtomicBoolean once = new AtomicBoolean(); + + Disposable timer = scheduler.scheduleDirect(() -> { + if (once.compareAndSet(false, true)) { + set.clear(); + if (other == null) { + s.onError(new TimeoutException()); + } else { + other.subscribe(new CompletableSubscriber() { + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + set.dispose(); + s.onError(e); + } + + @Override + public void onComplete() { + set.dispose(); + s.onComplete(); + } + + }); + } + } + }, timeout, unit); + + set.add(timer); + + source.subscribe(new CompletableSubscriber() { + + @Override + public void onSubscribe(Disposable d) { + set.add(d); + } + + @Override + public void onError(Throwable e) { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onError(e); + } else { + RxJavaPlugins.onError(e); + } + } + + @Override + public void onComplete() { + if (once.compareAndSet(false, true)) { + set.dispose(); + s.onComplete(); + } + } + + }); + } +} diff --git a/src/main/java/io/reactivex/internal/subscriptions/DisposableSubscription.java b/src/main/java/io/reactivex/internal/subscriptions/DisposableSubscription.java new file mode 100644 index 0000000000..f8a9e3131e --- /dev/null +++ b/src/main/java/io/reactivex/internal/subscriptions/DisposableSubscription.java @@ -0,0 +1,105 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.internal.subscriptions; + +import java.util.concurrent.atomic.AtomicReference; + +import org.reactivestreams.Subscription; + +import io.reactivex.disposables.Disposable; + +/** + * Subscription that ignores (but validates) requests and delegates cancel to a disposable instance. + */ +public final class DisposableSubscription extends AtomicReference implements Subscription { + /** */ + private static final long serialVersionUID = -2358839743797425727L; + /** The disposed state indicator. */ + static final Disposable DISPOSED = () -> { }; + + /** + * Constructs an empty DisposableSubscription. + */ + public DisposableSubscription() { + } + + /** + * Constructs a DisposableSubscription by wrapping the given Disposable. + * @param d the disposable to wrap, may be null + */ + public DisposableSubscription(Disposable d) { + this.lazySet(d); + } + + @Override + public void request(long n) { + if (SubscriptionHelper.validateRequest(n)) { + return; + } + } + + /** + * Sets a new disposable resource and disposes any old one. + * @param d the new disposable to set + * @return false if this Subscription has been cancelled + */ + public boolean setDisposable(Disposable d) { + for (;;) { + Disposable a = get(); + if (a == DISPOSED) { + if (d != null) { + d.dispose(); + } + return false; + } + if (compareAndSet(a, d)) { + if (a != null) { + a.dispose(); + } + return true; + } + } + } + + /** + * Replaces any existing disposable with a new disposable but doesn't dispose the old one. + * @param d the new disposable to set + * @return + */ + public boolean replaceDisposable(Disposable d) { + for (;;) { + Disposable a = get(); + if (a == DISPOSED) { + if (d != null) { + d.dispose(); + } + return false; + } + if (compareAndSet(a, d)) { + return true; + } + } + } + + @Override + public void cancel() { + Disposable d = get(); + if (d != DISPOSED) { + d = getAndSet(DISPOSED); + if (d != DISPOSED && d != null) { + d.dispose(); + } + } + } +} diff --git a/src/main/java/io/reactivex/subscribers/completable/CompletableSerializedSubscriber.java b/src/main/java/io/reactivex/subscribers/completable/CompletableSerializedSubscriber.java new file mode 100644 index 0000000000..98e7fa8c09 --- /dev/null +++ b/src/main/java/io/reactivex/subscribers/completable/CompletableSerializedSubscriber.java @@ -0,0 +1,52 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex.subscribers.completable; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import io.reactivex.Completable.CompletableSubscriber; +import io.reactivex.disposables.Disposable; + +public final class CompletableSerializedSubscriber implements CompletableSubscriber { + + final CompletableSubscriber actual; + + volatile int once; + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(CompletableSerializedSubscriber.class, "once"); + + public CompletableSerializedSubscriber(CompletableSubscriber actual) { + this.actual = actual; + } + + @Override + public void onSubscribe(Disposable d) { + actual.onSubscribe(d); + } + + @Override + public void onError(Throwable e) { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onError(e); + } + } + + @Override + public void onComplete() { + if (ONCE.compareAndSet(this, 0, 1)) { + actual.onComplete(); + } + } + +} diff --git a/src/test/java/io/reactivex/CompletableTest.java b/src/test/java/io/reactivex/CompletableTest.java new file mode 100644 index 0000000000..cb9f34eb1f --- /dev/null +++ b/src/test/java/io/reactivex/CompletableTest.java @@ -0,0 +1,3016 @@ +/** + * Copyright 2015 Netflix, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is + * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See + * the License for the specific language governing permissions and limitations under the License. + */ + +package io.reactivex; + +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.*; +import java.util.function.*; + +import org.junit.*; +import org.reactivestreams.Subscriber; + +import io.reactivex.Completable.*; +import io.reactivex.NbpObservable.NbpSubscriber; +import io.reactivex.disposables.*; +import io.reactivex.exceptions.TestException; +import io.reactivex.internal.disposables.EmptyDisposable; +import io.reactivex.plugins.RxJavaPlugins; +import io.reactivex.schedulers.*; +import io.reactivex.subjects.PublishSubject; +import io.reactivex.subscribers.TestSubscriber; +import io.reactivex.subscribers.nbp.NbpTestSubscriber; + +/** + * Test Completable methods and operators. + */ +public class CompletableTest { + /** + * Iterable that returns an Iterator that throws in its hasNext method. + */ + static final class IterableIteratorNextThrows implements Iterable { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return true; + } + + @Override + public Completable next() { + throw new TestException(); + } + }; + } + } + + /** + * Iterable that returns an Iterator that throws in its next method. + */ + static final class IterableIteratorHasNextThrows implements Iterable { + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + throw new TestException(); + } + + @Override + public Completable next() { + return null; + } + }; + } + } + + /** + * A class containing a completable instance and counts the number of subscribers. + */ + static final class NormalCompletable extends AtomicInteger { + /** */ + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.create(s -> { + getAndIncrement(); + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onComplete(); + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** + * A class containing a completable instance that emits a TestException and counts + * the number of subscribers. + */ + static final class ErrorCompletable extends AtomicInteger { + /** */ + private static final long serialVersionUID = 7192337844700923752L; + + public final Completable completable = Completable.create(s -> { + getAndIncrement(); + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(new TestException()); + }); + + /** + * Asserts the given number of subscriptions happened. + * @param n the expected number of subscriptions + */ + public void assertSubscriptions(int n) { + Assert.assertEquals(n, get()); + } + } + + /** A normal Completable object. */ + final NormalCompletable normal = new NormalCompletable(); + + /** An error Completable object. */ + final ErrorCompletable error = new ErrorCompletable(); + + @Test(timeout = 1000) + public void complete() { + Completable c = Completable.complete(); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatNull() { + Completable.concat((Completable[])null); + } + + @Test(timeout = 1000) + public void concatEmpty() { + Completable c = Completable.concat(); + + c.await(); + } + + @Test(timeout = 1000) + public void concatSingleSource() { + Completable c = Completable.concat(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatSingleSourceThrows() { + Completable c = Completable.concat(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void concatMultipleSources() { + Completable c = Completable.concat(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatMultipleOneThrows() { + Completable c = Completable.concat(normal.completable, error.completable, normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatMultipleOneIsNull() { + Completable c = Completable.concat(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void concatIterableEmpty() { + Completable c = Completable.concat(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatIterableNull() { + Completable.concat((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatIterableIteratorNull() { + Completable c = Completable.concat(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void concatIterableWithNull() { + Completable c = Completable.concat(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatIterableSingle() { + Completable c = Completable.concat(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void concatIterableMany() { + Completable c = Completable.concat(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatIterableOneThrows() { + Completable c = Completable.concat(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatIterableManyOneThrows() { + Completable c = Completable.concat(Arrays.asList(normal.completable, error.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIterableThrows() { + Completable c = Completable.concat(() -> { + throw new TestException(); + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorHasNextThrows() { + Completable c = Completable.concat(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void concatIterableIteratorNextThrows() { + Completable c = Completable.concat(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableEmpty() { + Completable c = Completable.concat(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableError() { + Completable c = Completable.concat(Observable.error(() -> new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableSingle() { + Completable c = Completable.concat(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableSingleThrows() { + Completable c = Completable.concat(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservableMany() { + Completable c = Completable.concat(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatObservableManyOneThrows() { + Completable c = Completable.concat(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void concatObservablePrefetch() { + List requested = new ArrayList<>(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(v -> requested.add(v)); + + Completable c = Completable.concat(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(expected = NullPointerException.class) + public void createNull() { + Completable.create(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void createOnSubscribeThrowsNPE() { + Completable c = Completable.create(s -> { throw new NullPointerException(); }); + + c.await(); + } + + @Test(timeout = 1000) + public void createOnSubscribeThrowsRuntimeException() { + try { + Completable c = Completable.create(s -> { + throw new TestException(); + }); + + c.await(); + + Assert.fail("Did not throw exception"); + } catch (NullPointerException ex) { + if (!(ex.getCause() instanceof TestException)) { + ex.printStackTrace(); + Assert.fail("Did not wrap the TestException but it returned: " + ex); + } + } + } + + @Test(timeout = 1000) + public void defer() { + Completable c = Completable.defer(() -> normal.completable); + + normal.assertSubscriptions(0); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(expected = NullPointerException.class) + public void deferNull() { + Completable.defer(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void deferReturnsNull() { + Completable c = Completable.defer(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void deferFunctionThrows() { + Completable c = Completable.defer(() -> { throw new TestException(); }); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void deferErrorSource() { + Completable c = Completable.defer(() -> error.completable); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void errorSupplierNull() { + Completable.error((Supplier)null); + } + + @Test(timeout = 1000, expected = TestException.class) + public void errorSupplierNormal() { + Completable c = Completable.error(() -> new TestException()); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void errorSupplierReturnsNull() { + Completable c = Completable.error(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void errorSupplierThrows() { + Completable c = Completable.error(() -> { throw new TestException(); }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void errorNull() { + Completable.error((Throwable)null); + } + + @Test(timeout = 1000, expected = TestException.class) + public void errorNormal() { + Completable c = Completable.error(new TestException()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromCallableNull() { + Completable.fromCallable(null); + } + + @Test(timeout = 1000) + public void fromCallableNormal() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(() -> calls.getAndIncrement()); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromCallableThrows() { + Completable c = Completable.fromCallable(() -> { throw new TestException(); }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromFlowableNull() { + Completable.fromFlowable(null); + } + + @Test(timeout = 1000) + public void fromFlowableEmpty() { + Completable c = Completable.fromFlowable(Observable.empty()); + + c.await(); + } + + @Test(timeout = 5000) + public void fromFlowableSome() { + for (int n = 1; n < 10000; n *= 10) { + Completable c = Completable.fromFlowable(Observable.range(1, n)); + + c.await(); + } + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromFlowableError() { + Completable c = Completable.fromFlowable(Observable.error(() -> new TestException())); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromFutureNull() { + Completable.fromFuture(null); + } + + @Test(timeout = 1000) + public void fromFutureNormal() { + CompletableFuture f = new CompletableFuture<>(); + + Completable c = Completable.fromFuture(f); + + f.complete(1); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromFutureThrows() { + CompletableFuture f = new CompletableFuture<>(); + + Completable c = Completable.fromFuture(f); + + f.completeExceptionally(new TestException()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromNbpObservableNull() { + Completable.fromNbpObservable(null); + } + + @Test(timeout = 1000) + public void fromNbpObservableEmpty() { + Completable c = Completable.fromNbpObservable(NbpObservable.empty()); + + c.await(); + } + + @Test(timeout = 5000) + public void fromNbpObservableSome() { + for (int n = 1; n < 10000; n *= 10) { + Completable c = Completable.fromNbpObservable(NbpObservable.range(1, n)); + + c.await(); + } + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromNbpObservableError() { + Completable c = Completable.fromNbpObservable(NbpObservable.error(() -> new TestException())); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromRunnableNull() { + Completable.fromRunnable(null); + } + + @Test(timeout = 1000) + public void fromRunnableNormal() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromRunnable(() -> calls.getAndIncrement()); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromRunnableThrows() { + Completable c = Completable.fromRunnable(() -> { throw new TestException(); }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void fromSingleNull() { + Completable.fromSingle(null); + } + + @Test(timeout = 1000) + public void fromSingleNormal() { + Completable c = Completable.fromSingle(Single.just(1)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void fromSingleThrows() { + Completable c = Completable.fromSingle(Single.error(() -> new TestException())); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeNull() { + Completable.merge((Completable[])null); + } + + @Test(timeout = 1000) + public void mergeEmpty() { + Completable c = Completable.merge(); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeSingleSource() { + Completable c = Completable.merge(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeSingleSourceThrows() { + Completable c = Completable.merge(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeMultipleSources() { + Completable c = Completable.merge(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeMultipleOneThrows() { + Completable c = Completable.merge(normal.completable, error.completable, normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeMultipleOneIsNull() { + Completable c = Completable.merge(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeIterableEmpty() { + Completable c = Completable.merge(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeIterableNull() { + Completable.merge((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeIterableIteratorNull() { + Completable c = Completable.merge(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeIterableWithNull() { + Completable c = Completable.merge(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeIterableSingle() { + Completable c = Completable.merge(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void mergeIterableMany() { + Completable c = Completable.merge(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeIterableOneThrows() { + Completable c = Completable.merge(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeIterableManyOneThrows() { + Completable c = Completable.merge(Arrays.asList(normal.completable, error.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIterableThrows() { + Completable c = Completable.merge(() -> { + throw new TestException(); + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorHasNextThrows() { + Completable c = Completable.merge(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeIterableIteratorNextThrows() { + Completable c = Completable.merge(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableEmpty() { + Completable c = Completable.merge(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableError() { + Completable c = Completable.merge(Observable.error(() -> new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableSingle() { + Completable c = Completable.merge(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableSingleThrows() { + Completable c = Completable.merge(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableMany() { + Completable c = Completable.merge(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeObservableManyOneThrows() { + Completable c = Completable.merge(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeObservableMaxConcurrent() { + List requested = new ArrayList<>(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(v -> requested.add(v)); + + Completable c = Completable.merge(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorNull() { + Completable.mergeDelayError((Completable[])null); + } + + @Test(timeout = 1000) + public void mergeDelayErrorEmpty() { + Completable c = Completable.mergeDelayError(); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorSingleSource() { + Completable c = Completable.mergeDelayError(normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorSingleSourceThrows() { + Completable c = Completable.mergeDelayError(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorMultipleSources() { + Completable c = Completable.mergeDelayError(normal.completable, normal.completable, normal.completable); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000) + public void mergeDelayErrorMultipleOneThrows() { + Completable c = Completable.mergeDelayError(normal.completable, error.completable, normal.completable); + + try { + c.await(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorMultipleOneIsNull() { + Completable c = Completable.mergeDelayError(normal.completable, null); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableEmpty() { + Completable c = Completable.mergeDelayError(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeDelayErrorIterableNull() { + Completable.mergeDelayError((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorIterableIteratorNull() { + Completable c = Completable.mergeDelayError(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void mergeDelayErrorIterableWithNull() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, (Completable)null)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableSingle() { + Completable c = Completable.mergeDelayError(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableMany() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorIterableOneThrows() { + Completable c = Completable.mergeDelayError(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorIterableManyOneThrows() { + Completable c = Completable.mergeDelayError(Arrays.asList(normal.completable, error.completable, normal.completable)); + + try { + c.await(); + } catch (TestException ex) { + normal.assertSubscriptions(2); + } + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIterableThrows() { + Completable c = Completable.mergeDelayError(() -> { + throw new TestException(); + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorHasNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void mergeDelayErrorIterableIteratorNextThrows() { + Completable c = Completable.mergeDelayError(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableEmpty() { + Completable c = Completable.mergeDelayError(Observable.empty()); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableError() { + Completable c = Completable.mergeDelayError(Observable.error(() -> new TestException())); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableSingle() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableSingleThrows() { + Completable c = Completable.mergeDelayError(Observable.just(error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableMany() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable).repeat(3)); + + c.await(); + + normal.assertSubscriptions(3); + } + + @Test(timeout = 1000, expected = TestException.class) + public void mergeDelayErrorObservableManyOneThrows() { + Completable c = Completable.mergeDelayError(Observable.just(normal.completable, error.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void mergeDelayErrorObservableMaxConcurrent() { + List requested = new ArrayList<>(); + Observable cs = Observable + .just(normal.completable) + .repeat(10) + .doOnRequest(v -> requested.add(v)); + + Completable c = Completable.mergeDelayError(cs, 5); + + c.await(); + + // FIXME this request pattern looks odd because all 10 completions trigger 1 requests + Assert.assertEquals(Arrays.asList(5L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L), requested); + } + + @Test(timeout = 1000) + public void never() { + AtomicBoolean onSubscribeCalled = new AtomicBoolean(); + AtomicInteger calls = new AtomicInteger(); + Completable.never().subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + onSubscribeCalled.set(true); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + }); + + Assert.assertTrue("onSubscribe not called", onSubscribeCalled.get()); + Assert.assertEquals("There were calls to onXXX methods", 0, calls.get()); + } + + @Test(timeout = 1500) + public void timer() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS); + + c.await(); + } + + @Test(timeout = 1500) + public void timerNewThread() { + Completable c = Completable.timer(500, TimeUnit.MILLISECONDS, Schedulers.newThread()); + + c.await(); + } + + @Test(timeout = 1000) + public void timerTestScheduler() { + TestScheduler scheduler = Schedulers.test(); + + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS, scheduler); + + AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + + @Override + public void onError(Throwable e) { + RxJavaPlugins.onError(e); + } + }); + + scheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS); + + Assert.assertEquals(0, calls.get()); + + scheduler.advanceTimeBy(200, TimeUnit.MILLISECONDS); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 2000) + public void timerCancel() throws InterruptedException { + Completable c = Completable.timer(250, TimeUnit.MILLISECONDS); + + MultipleAssignmentDisposable mad = new MultipleAssignmentDisposable(); + AtomicInteger calls = new AtomicInteger(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + mad.set(d); + } + + @Override + public void onError(Throwable e) { + calls.getAndIncrement(); + } + + @Override + public void onComplete() { + calls.getAndIncrement(); + } + }); + + Thread.sleep(100); + + mad.dispose(); + + Thread.sleep(200); + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void timerUnitNull() { + Completable.timer(1, null); + } + + @Test(expected = NullPointerException.class) + public void timerSchedulerNull() { + Completable.timer(1, TimeUnit.SECONDS, null); + } + + @Test(timeout = 1000) + public void usingNormalEager() { + AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(() -> 1, v -> normal.completable, d -> dispose.set(d)); + + AtomicBoolean disposedFirst = new AtomicBoolean(); + AtomicReference error = new AtomicReference<>(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onComplete() { + disposedFirst.set(dispose.get() != 0); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertTrue("Not disposed first", disposedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void usingNormalLazy() { + AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(() -> 1, v -> normal.completable, d -> dispose.set(d), false); + + AtomicBoolean disposedFirst = new AtomicBoolean(); + AtomicReference error = new AtomicReference<>(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.lazySet(e); + } + + @Override + public void onComplete() { + disposedFirst.set(dispose.get() != 0); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertFalse("Disposed first", disposedFirst.get()); + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void usingErrorEager() { + AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(() -> 1, v -> error.completable, d -> dispose.set(d)); + + AtomicBoolean disposedFirst = new AtomicBoolean(); + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + disposedFirst.set(dispose.get() != 0); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertTrue("Not disposed first", disposedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test(timeout = 1000) + public void usingErrorLazy() { + AtomicInteger dispose = new AtomicInteger(); + + Completable c = Completable.using(() -> 1, v -> error.completable, d -> dispose.set(d), false); + + AtomicBoolean disposedFirst = new AtomicBoolean(); + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + disposedFirst.set(dispose.get() != 0); + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + Assert.assertEquals(1, dispose.get()); + Assert.assertFalse("Disposed first", disposedFirst.get()); + Assert.assertFalse(complete.get()); + } + + @Test(expected = NullPointerException.class) + public void usingResourceSupplierNull() { + Completable.using(null, v -> normal.completable, v -> { }); + } + + @Test(expected = NullPointerException.class) + public void usingMapperNull() { + Completable.using(() -> 1, null, v -> { }); + } + + @Test(expected = NullPointerException.class) + public void usingMapperReturnsNull() { + Completable c = Completable.using(() -> 1, v -> null, v -> { }); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void usingDisposeNull() { + Completable.using(() -> 1, v -> normal.completable, null); + } + + @Test(expected = TestException.class) + public void usingResourceThrows() { + Completable c = Completable.using(() -> { throw new TestException(); }, + v -> normal.completable, v -> { }); + + c.await(); + } + + @Test(expected = TestException.class) + public void usingMapperThrows() { + Completable c = Completable.using(() -> 1, + v -> { throw new TestException(); }, v -> { }); + + c.await(); + } + + @Test(expected = TestException.class) + public void usingDisposerThrows() { + Completable c = Completable.using(() -> 1, + v -> normal.completable, v -> { throw new TestException(); }); + + c.await(); + } + + @Test(timeout = 1000) + public void composeNormal() { + Completable c = error.completable.compose(n -> n.onErrorComplete()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void composeNull() { + error.completable.compose(null); + } + + @Test(timeout = 1000) + public void concatWithNormal() { + Completable c = normal.completable.concatWith(normal.completable); + + c.await(); + + normal.assertSubscriptions(2); + } + + @Test(timeout = 1000, expected = TestException.class) + public void concatWithError() { + Completable c = normal.completable.concatWith(error.completable); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void concatWithNull() { + normal.completable.concatWith(null); + } + + @Test(expected = NullPointerException.class) + public void delayUnitNull() { + normal.completable.delay(1, null); + } + + @Test(expected = NullPointerException.class) + public void delaySchedulerNull() { + normal.completable.delay(1, TimeUnit.SECONDS, null); + } + + @Test(timeout = 1000) + public void delayNormal() throws InterruptedException { + Completable c = normal.completable.delay(250, TimeUnit.MILLISECONDS); + + AtomicBoolean done = new AtomicBoolean(); + AtomicReference error = new AtomicReference<>(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(200); + + Assert.assertTrue("Not done", done.get()); + + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void delayErrorImmediately() throws InterruptedException { + Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS); + + AtomicBoolean done = new AtomicBoolean(); + AtomicReference error = new AtomicReference<>(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + Assert.assertTrue(error.get().toString(), error.get() instanceof TestException); + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + + Thread.sleep(200); + } + + @Test(timeout = 1000) + public void delayErrorToo() throws InterruptedException { + Completable c = error.completable.delay(250, TimeUnit.MILLISECONDS, Schedulers.computation(), true); + + AtomicBoolean done = new AtomicBoolean(); + AtomicReference error = new AtomicReference<>(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + error.set(e); + } + + @Override + public void onComplete() { + done.set(true); + } + }); + + Thread.sleep(100); + + Assert.assertFalse("Already done", done.get()); + Assert.assertNull(error.get()); + + Thread.sleep(200); + + Assert.assertFalse("Already done", done.get()); + Assert.assertTrue(error.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void doOnCompleteNormal() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnComplete(() -> calls.getAndIncrement()); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void doOnCompleteError() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnComplete(() -> calls.getAndIncrement()); + + try { + c.await(); + Assert.fail("Failed to throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(0, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnCompleteNull() { + normal.completable.doOnComplete(null); + } + + @Test(timeout = 1000, expected = TestException.class) + public void doOnCompleteThrows() { + Completable c = normal.completable.doOnComplete(() -> { throw new TestException(); }); + + c.await(); + } + + @Test(timeout = 1000) + public void doOnDisposeNormalDoesntCall() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnDispose(() -> calls.getAndIncrement()); + + c.await(); + + Assert.assertEquals(0, calls.get()); + } + + @Test(timeout = 1000) + public void doOnDisposeErrorDoesntCall() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnDispose(() -> calls.getAndIncrement()); + + try { + c.await(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + Assert.assertEquals(0, calls.get()); + } + + @Test(timeout = 1000) + public void doOnDisposeChildCancels() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnDispose(() -> calls.getAndIncrement()); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + + Assert.assertEquals(1, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnDisposeNull() { + normal.completable.doOnDispose(null); + } + + @Test(timeout = 1000) + public void doOnDisposeThrows() { + Completable c = normal.completable.doOnDispose(() -> { throw new TestException(); }); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + d.dispose(); + } + + @Override + public void onError(Throwable e) { + // ignored + } + + @Override + public void onComplete() { + // ignored + } + }); + } + + @Test(timeout = 1000) + public void doOnErrorNoError() { + AtomicReference error = new AtomicReference<>(); + + Completable c = normal.completable.doOnError(error::set); + + c.await(); + + Assert.assertNull(error.get()); + } + + @Test(timeout = 1000) + public void doOnErrorHasError() { + AtomicReference err = new AtomicReference<>(); + + Completable c = error.completable.doOnError(err::set); + + try { + c.await(); + Assert.fail("Did not throw exception"); + } catch (Throwable e) { + // expected + } + + Assert.assertTrue(err.get() instanceof TestException); + } + + @Test(expected = NullPointerException.class) + public void doOnErrorNull() { + normal.completable.doOnError(null); + } + + @Test(timeout = 1000) + public void doOnErrorThrows() { + Completable c = error.completable.doOnError(e -> { throw new IllegalStateException(); }); + + try { + c.await(); + } catch (IllegalStateException ex) { + Throwable[] a = ex.getSuppressed(); + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void doOnSubscribeNormal() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnSubscribe(s -> calls.getAndIncrement()); + + for (int i = 0; i < 10; i++) { + c.await(); + } + + Assert.assertEquals(10, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void doOnSubscribeNull() { + normal.completable.doOnSubscribe(null); + } + + @Test(expected = TestException.class) + public void doOnSubscribeThrows() { + Completable c = normal.completable.doOnSubscribe(d -> { throw new TestException(); }); + + c.await(); + } + + @Test(timeout = 1000) + public void doOnTerminateNormal() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = normal.completable.doOnTerminate(() -> calls.getAndIncrement()); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void doOnTerminateError() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = error.completable.doOnTerminate(() -> calls.getAndIncrement()); + + try { + c.await(); + Assert.fail("Did dot throw exception"); + } catch (TestException ex) { + // expected + } + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void finallyDoNormal() { + AtomicBoolean doneAfter = new AtomicBoolean(); + AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable.finallyDo(() -> doneAfter.set(complete.get())); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + complete.set(true); + } + }); + + c.await(); + + Assert.assertTrue("Not completed", complete.get()); + Assert.assertTrue("Finally called before onComplete", doneAfter.get()); + } + + @Test(timeout = 1000) + public void finallyDoWithError() { + AtomicBoolean doneAfter = new AtomicBoolean(); + + Completable c = error.completable.finallyDo(() -> doneAfter.set(true)); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + // expected + } + + Assert.assertFalse("FinallyDo called", doneAfter.get()); + } + + @Test(expected = NullPointerException.class) + public void finallyDoNull() { + normal.completable.finallyDo(null); + } + + @Test(timeout = 1000) + public void getNormal() { + Assert.assertNull(normal.completable.get()); + } + + @Test(timeout = 1000) + public void getError() { + Assert.assertTrue(error.completable.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void getTimeout() { + try { + Completable.never().get(100, TimeUnit.MILLISECONDS); + } catch (RuntimeException ex) { + if (!(ex.getCause() instanceof TimeoutException)) { + Assert.fail("Wrong exception cause: " + ex.getCause()); + } + } + } + + @Test(expected = NullPointerException.class) + public void getNullUnit() { + normal.completable.get(1, null); + } + + @Test(expected = NullPointerException.class) + public void liftNull() { + normal.completable.lift(null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void liftReturnsNull() { + Completable c = normal.completable.lift(v -> null); + + c.await(); + } + + final static class CompletableOperatorSwap implements CompletableOperator { + @Override + public CompletableSubscriber apply(CompletableSubscriber v) { + return new CompletableSubscriber() { + + @Override + public void onComplete() { + v.onError(new TestException()); + } + + @Override + public void onError(Throwable e) { + v.onComplete(); + } + + @Override + public void onSubscribe(Disposable d) { + v.onSubscribe(d); + } + + }; + } + } + @Test(timeout = 1000, expected = TestException.class) + public void liftOnCompleteError() { + Completable c = normal.completable.lift(new CompletableOperatorSwap()); + + c.await(); + } + + @Test(timeout = 1000) + public void liftOnErrorComplete() { + Completable c = error.completable.lift(new CompletableOperatorSwap()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void mergeWithNull() { + normal.completable.mergeWith(null); + } + + @Test(timeout = 1000) + public void mergeWithNormal() { + Completable c = normal.completable.mergeWith(normal.completable); + + c.await(); + + normal.assertSubscriptions(2); + } + + @Test(expected = NullPointerException.class) + public void observeOnNull() { + normal.completable.observeOn(null); + } + + @Test(timeout = 1000) + public void observeOnNormal() throws InterruptedException { + AtomicReference name = new AtomicReference<>(); + AtomicReference err = new AtomicReference<>(); + CountDownLatch cdl = new CountDownLatch(1); + + Completable c = normal.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertNull(err.get()); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void observeOnError() throws InterruptedException { + AtomicReference name = new AtomicReference<>(); + AtomicReference err = new AtomicReference<>(); + CountDownLatch cdl = new CountDownLatch(1); + + Completable c = error.completable.observeOn(Schedulers.computation()); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + + } + + @Override + public void onComplete() { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + } + + @Override + public void onError(Throwable e) { + name.set(Thread.currentThread().getName()); + err.set(e); + cdl.countDown(); + } + }); + + cdl.await(); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void onErrorComplete() { + Completable c = error.completable.onErrorComplete(); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void onErrorCompleteFalse() { + Completable c = error.completable.onErrorComplete(e -> e instanceof IllegalStateException); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void onErrorCompleteNull() { + error.completable.onErrorComplete(null); + } + + @Test(expected = NullPointerException.class) + public void onErrorResumeNextNull() { + error.completable.onErrorResumeNext(null); + } + + @Test(timeout = 1000) + public void onErrorResumeNextFunctionReturnsNull() { + Completable c = error.completable.onErrorResumeNext(e -> null); + + try { + c.await(); + Assert.fail("Did not throw an exception"); + } catch (NullPointerException ex) { + Throwable[] a = ex.getSuppressed(); + + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void onErrorResumeNextFunctionThrows() { + Completable c = error.completable.onErrorResumeNext(e -> { throw new TestException(); }); + + try { + c.await(); + Assert.fail("Did not throw an exception"); + } catch (TestException ex) { + Throwable[] a = ex.getSuppressed(); + + Assert.assertEquals(1, a.length); + Assert.assertTrue(a[0] instanceof TestException); + } + } + + @Test(timeout = 1000) + public void onErrorResumeNextNormal() { + Completable c = error.completable.onErrorResumeNext(v -> normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void onErrorResumeNextError() { + Completable c = error.completable.onErrorResumeNext(v -> error.completable); + + c.await(); + } + + @Test(timeout = 2000) + public void repeatNormal() { + AtomicReference err = new AtomicReference<>(); + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(() -> { + calls.getAndIncrement(); + Thread.sleep(100); + return null; + }).repeat(); + + c.subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + Schedulers.single().scheduleDirect(d::dispose, 550, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + err.set(e); + } + + @Override + public void onComplete() { + + } + }); + + Assert.assertEquals(6, calls.get()); + Assert.assertNull(err.get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void repeatError() { + Completable c = error.completable.repeat(); + + c.await(); + } + + @Test(timeout = 1000) + public void repeat5Times() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(() -> { + calls.getAndIncrement(); + return null; + }).repeat(5); + + c.await(); + + Assert.assertEquals(5, calls.get()); + } + + @Test(timeout = 1000) + public void repeat1Time() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(() -> { + calls.getAndIncrement(); + return null; + }).repeat(1); + + c.await(); + + Assert.assertEquals(1, calls.get()); + } + + @Test(timeout = 1000) + public void repeat0Time() { + AtomicInteger calls = new AtomicInteger(); + + Completable c = Completable.fromCallable(() -> { + calls.getAndIncrement(); + return null; + }).repeat(0); + + c.await(); + + Assert.assertEquals(0, calls.get()); + } + + @Test(timeout = 1000) + public void repeatUntilNormal() { + AtomicInteger calls = new AtomicInteger(); + AtomicInteger times = new AtomicInteger(5); + + Completable c = Completable.fromCallable(() -> { + calls.getAndIncrement(); + return null; + }).repeatUntil(() -> times.decrementAndGet() == 0); + + c.await(); + + Assert.assertEquals(5, calls.get()); + } + + @Test(expected = NullPointerException.class) + public void repeatUntilNull() { + normal.completable.repeatUntil(null); + } + + @Test(expected = NullPointerException.class) + public void repeatWhenNull() { + normal.completable.repeatWhen(null); + } + + @Test(timeout = 1000) + public void retryNormal() { + Completable c = normal.completable.retry(); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void retry5Times() { + AtomicInteger calls = new AtomicInteger(5); + Completable c = Completable.fromRunnable(() -> { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + }).retry(); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void retryBiPredicate5Times() { + Completable c = error.completable.retry((n, e) -> n < 5); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void retryTimes5Error() { + Completable c = error.completable.retry(5); + + c.await(); + } + + @Test(timeout = 1000) + public void retryTimes5Normal() { + AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromRunnable(() -> { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + }).retry(5); + + c.await(); + } + + @Test(expected = IllegalArgumentException.class) + public void retryNegativeTimes() { + normal.completable.retry(-1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void retryPredicateError() { + Completable c = error.completable.retry(e -> false); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void retryPredicateNull() { + error.completable.retry((Predicate)null); + } + + @Test(timeout = 1000) + public void retryPredicate5Times() { + AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromRunnable(() -> { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + }).retry(e -> true); + + c.await(); + } + + @Test(timeout = 1000) + public void retryWhen5Times() { + AtomicInteger calls = new AtomicInteger(5); + + Completable c = Completable.fromRunnable(() -> { + if (calls.decrementAndGet() != 0) { + throw new TestException(); + } + }).retryWhen(o -> o); + + c.await(); + } + + @Test(timeout = 1000) + public void subscribe() throws InterruptedException { + AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(100, TimeUnit.MILLISECONDS) + .doOnComplete(() -> complete.set(true)); + + c.subscribe(); + + Thread.sleep(150); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeDispose() throws InterruptedException { + AtomicBoolean complete = new AtomicBoolean(); + + Completable c = normal.completable + .delay(200, TimeUnit.MILLISECONDS) + .doOnComplete(() -> complete.set(true)); + + Disposable d = c.subscribe(); + + Thread.sleep(100); + + d.dispose(); + + Thread.sleep(150); + + Assert.assertFalse("Completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksNormal() { + AtomicReference err = new AtomicReference<>(); + AtomicBoolean complete = new AtomicBoolean(); + normal.completable.subscribe(e -> err.set(e), () -> complete.set(true)); + + Assert.assertNull(err.get()); + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksError() { + AtomicReference err = new AtomicReference<>(); + AtomicBoolean complete = new AtomicBoolean(); + error.completable.subscribe(e -> err.set(e), () -> complete.set(true)); + + Assert.assertTrue(err.get() instanceof TestException); + Assert.assertFalse("Not completed", complete.get()); + } + + @Test(expected = NullPointerException.class) + public void subscribeTwoCallbacksFirstNull() { + normal.completable.subscribe(null, () -> { }); + } + + @Test(expected = NullPointerException.class) + public void subscribeTwoCallbacksSecondNull() { + normal.completable.subscribe(null, () -> { }); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksCompleteThrows() { + AtomicReference err = new AtomicReference<>(); + normal.completable.subscribe(e -> err.set(e), () -> { throw new TestException(); }); + + Assert.assertTrue(String.valueOf(err.get()), err.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void subscribeTwoCallbacksOnErrorThrows() { + error.completable.subscribe(e -> { throw new TestException(); }, () -> { }); + } + + @Test(timeout = 1000) + public void subscribeNbpSubscriberNormal() { + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + normal.completable.subscribe(ts); + + ts.assertComplete(); + ts.assertNoValues(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void subscribeNbpSubscriberError() { + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + error.completable.subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoValues(); + ts.assertError(TestException.class); + } + + @Test(timeout = 1000) + public void subscribeRunnableNormal() { + AtomicBoolean run = new AtomicBoolean(); + + normal.completable.subscribe(() -> run.set(true)); + + Assert.assertTrue("Not completed", run.get()); + } + + @Test(timeout = 1000) + public void subscribeRunnableError() { + AtomicBoolean run = new AtomicBoolean(); + + error.completable.subscribe(() -> run.set(true)); + + Assert.assertFalse("Completed", run.get()); + } + + @Test(expected = NullPointerException.class) + public void subscribeRunnableNull() { + normal.completable.subscribe((Runnable)null); + } + + @Test(expected = NullPointerException.class) + public void subscribeSubscriberNull() { + normal.completable.subscribe((Subscriber)null); + } + + @Test(expected = NullPointerException.class) + public void subscribeNbpSubscriberNull() { + normal.completable.subscribe((NbpSubscriber)null); + } + + @Test(expected = NullPointerException.class) + public void subscribeCompletableSubscriberNull() { + normal.completable.subscribe((CompletableSubscriber)null); + } + + @Test(timeout = 1000) + public void subscribeSubscriberNormal() { + TestSubscriber ts = new TestSubscriber<>(); + + normal.completable.subscribe(ts); + + ts.assertComplete(); + ts.assertNoValues(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void subscribeSubscriberError() { + TestSubscriber ts = new TestSubscriber<>(); + + error.completable.subscribe(ts); + + ts.assertNotComplete(); + ts.assertNoValues(); + ts.assertError(TestException.class); + } + + @Test(expected = NullPointerException.class) + public void subscribeOnNull() { + normal.completable.subscribeOn(null); + } + + @Test(timeout = 1000) + public void subscribeOnNormal() { + AtomicReference name = new AtomicReference<>(); + + Completable c = Completable.create(s -> { + name.set(Thread.currentThread().getName()); + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onComplete(); + }).subscribeOn(Schedulers.computation()); + + c.await(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void subscribeOnError() { + AtomicReference name = new AtomicReference<>(); + + Completable c = Completable.create(s -> { + name.set(Thread.currentThread().getName()); + s.onSubscribe(EmptyDisposable.INSTANCE); + s.onError(new TestException()); + }).subscribeOn(Schedulers.computation()); + + try { + c.await(); + Assert.fail("No exception thrown"); + } catch (TestException ex) { + // expected + } + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(timeout = 1000) + public void timeoutEmitError() { + Throwable e = Completable.never().timeout(100, TimeUnit.MILLISECONDS).get(); + + Assert.assertTrue(e instanceof TimeoutException); + } + + @Test(timeout = 1000) + public void timeoutSwitchNormal() { + Completable c = Completable.never().timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void timeoutTimerCancelled() throws InterruptedException { + Completable c = Completable.fromCallable(() -> { + Thread.sleep(50); + return null; + }).timeout(100, TimeUnit.MILLISECONDS, normal.completable); + + c.await(); + + Thread.sleep(100); + + normal.assertSubscriptions(0); + } + + @Test(expected = NullPointerException.class) + public void timeoutUnitNull() { + normal.completable.timeout(1, null); + } + + @Test(expected = NullPointerException.class) + public void timeoutSchedulerNull() { + normal.completable.timeout(1, TimeUnit.SECONDS, (Scheduler)null); + } + + @Test(expected = NullPointerException.class) + public void timeoutOtherNull() { + normal.completable.timeout(1, TimeUnit.SECONDS, (Completable)null); + } + + @Test(timeout = 1000) + public void toNormal() { + Observable flow = normal.completable.to(Completable::toFlowable); + + flow.toBlocking().forEach(e -> { }); + } + + @Test(expected = NullPointerException.class) + public void toNull() { + normal.completable.to(null); + } + + @Test(timeout = 1000) + public void toFlowableNormal() { + normal.completable.toFlowable().toBlocking().forEach(e -> { }); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toFlowableError() { + error.completable.toFlowable().toBlocking().forEach(e -> { }); + } + + @Test(timeout = 1000) + public void toNbpObservableNormal() { + normal.completable.toNbpObservable().toBlocking().forEach(e -> { }); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toNbpObservableError() { + error.completable.toNbpObservable().toBlocking().forEach(e -> { }); + } + + @Test(timeout = 1000) + public void toSingleSupplierNormal() { + Assert.assertEquals((Integer)1, normal.completable.toSingle(() -> 1).get()); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toSingleSupplierError() { + error.completable.toSingle(() -> 1).get(); + } + + @Test(expected = NullPointerException.class) + public void toSingleSupplierNull() { + normal.completable.toSingle(null); + } + + @Test(expected = NullPointerException.class) + public void toSingleSupplierReturnsNull() { + normal.completable.toSingle(() -> null).get(); + } + + @Test(expected = TestException.class) + public void toSingleSupplierThrows() { + normal.completable.toSingle(() -> { throw new TestException(); }).get(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void toSingleDefaultError() { + error.completable.toSingleDefault(1).get(); + } + + @Test(timeout = 1000) + public void toSingleDefaultNormal() { + Assert.assertEquals((Integer)1, normal.completable.toSingleDefault(1).get()); + } + + @Test(expected = NullPointerException.class) + public void toSingleDefaultNull() { + normal.completable.toSingleDefault(null); + } + + @Test(timeout = 1000) + public void unsubscribeOnNormal() throws InterruptedException { + AtomicReference name = new AtomicReference<>(); + CountDownLatch cdl = new CountDownLatch(1); + + normal.completable.delay(1, TimeUnit.SECONDS) + .doOnDispose(() -> { + name.set(Thread.currentThread().getName()); + cdl.countDown(); + }) + .unsubscribeOn(Schedulers.computation()) + .subscribe(new CompletableSubscriber() { + @Override + public void onSubscribe(Disposable d) { + Schedulers.single().scheduleDirect(() -> { + d.dispose(); + }, 100, TimeUnit.MILLISECONDS); + } + + @Override + public void onError(Throwable e) { + + } + + @Override + public void onComplete() { + + } + }); + + cdl.await(); + + Assert.assertTrue(name.get().startsWith("RxComputation")); + } + + @Test(expected = NullPointerException.class) + public void ambArrayNull() { + Completable.amb((Completable[])null); + } + + @Test(timeout = 1000) + public void ambArrayEmpty() { + Completable c = Completable.amb(); + + c.await(); + } + + @Test(timeout = 1000) + public void ambArraySingleNormal() { + Completable c = Completable.amb(normal.completable); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambArraySingleError() { + Completable c = Completable.amb(error.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void ambArrayOneFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = Completable.amb(c1, c2); + + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(() -> complete.set(true)); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps1.onComplete(); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambArrayOneFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = Completable.amb(c1, c2); + + AtomicReference complete = new AtomicReference<>(); + + c.subscribe(complete::set, () -> { }); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void ambArraySecondFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = Completable.amb(c1, c2); + + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(() -> complete.set(true)); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps2.onComplete(); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambArraySecondFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = Completable.amb(c1, c2); + + AtomicReference complete = new AtomicReference<>(); + + c.subscribe(complete::set, () -> { }); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambMultipleOneIsNull() { + Completable c = Completable.amb(null, normal.completable); + + c.await(); + } + + @Test(timeout = 1000) + public void ambIterableEmpty() { + Completable c = Completable.amb(Collections.emptyList()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void ambIterableNull() { + Completable.amb((Iterable)null); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambIterableIteratorNull() { + Completable c = Completable.amb(() -> null); + + c.await(); + } + + @Test(timeout = 1000, expected = NullPointerException.class) + public void ambIterableWithNull() { + Completable c = Completable.amb(Arrays.asList(null, normal.completable)); + + c.await(); + } + + @Test(timeout = 1000) + public void ambIterableSingle() { + Completable c = Completable.amb(Collections.singleton(normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void ambIterableMany() { + Completable c = Completable.amb(Arrays.asList(normal.completable, normal.completable, normal.completable)); + + c.await(); + + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambIterableOneThrows() { + Completable c = Completable.amb(Collections.singleton(error.completable)); + + c.await(); + } + + @Test(timeout = 1000, expected = TestException.class) + public void ambIterableManyOneThrows() { + Completable c = Completable.amb(Arrays.asList(error.completable, normal.completable)); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIterableThrows() { + Completable c = Completable.amb(() -> { + throw new TestException(); + }); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIteratorHasNextThrows() { + Completable c = Completable.amb(new IterableIteratorHasNextThrows()); + + c.await(); + } + + @Test(expected = TestException.class) + public void ambIterableIteratorNextThrows() { + Completable c = Completable.amb(new IterableIteratorNextThrows()); + + c.await(); + } + + @Test(expected = NullPointerException.class) + public void ambWithNull() { + normal.completable.ambWith(null); + } + + @Test(timeout = 1000) + public void ambWithArrayOneFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = c1.ambWith(c2); + + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(() -> complete.set(true)); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps1.onComplete(); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambWithArrayOneFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = c1.ambWith(c2); + + AtomicReference complete = new AtomicReference<>(); + + c.subscribe(complete::set, () -> { }); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps1.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void ambWithArraySecondFires() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = c1.ambWith(c2); + + AtomicBoolean complete = new AtomicBoolean(); + + c.subscribe(() -> complete.set(true)); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps2.onComplete(); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get()); + } + + @Test(timeout = 1000) + public void ambWithArraySecondFiresError() { + PublishSubject ps1 = PublishSubject.create(); + PublishSubject ps2 = PublishSubject.create(); + + Completable c1 = Completable.fromFlowable(ps1); + + Completable c2 = Completable.fromFlowable(ps2); + + Completable c = c1.ambWith(c2); + + AtomicReference complete = new AtomicReference<>(); + + c.subscribe(complete::set, () -> { }); + + Assert.assertTrue("First subject no subscribers", ps1.hasSubscribers()); + Assert.assertTrue("Second subject no subscribers", ps2.hasSubscribers()); + + ps2.onError(new TestException()); + + Assert.assertFalse("First subject has subscribers", ps1.hasSubscribers()); + Assert.assertFalse("Second subject has subscribers", ps2.hasSubscribers()); + + Assert.assertTrue("Not completed", complete.get() instanceof TestException); + } + + @Test(timeout = 1000) + public void startWithCompletableNormal() { + AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .startWith(Completable.fromCallable(() -> { + run.set(normal.get() == 0); + return null; + })); + + c.await(); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void startWithCompletableError() { + Completable c = normal.completable.startWith(error.completable); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(0); + error.assertSubscriptions(1); + } + } + + @Test(timeout = 1000) + public void startWithFlowableNormal() { + AtomicBoolean run = new AtomicBoolean(); + Observable c = normal.completable + .startWith(Observable.fromCallable(() -> { + run.set(normal.get() == 0); + return 1; + })); + + TestSubscriber ts = new TestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void startWithFlowableError() { + Observable c = normal.completable + .startWith(Observable.error(new TestException())); + + TestSubscriber ts = new TestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(0); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test(timeout = 1000) + public void startWithNbpObservableNormal() { + AtomicBoolean run = new AtomicBoolean(); + NbpObservable c = normal.completable + .startWith(NbpObservable.fromCallable(() -> { + run.set(normal.get() == 0); + return 1; + })); + + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertTrue("Did not start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void startWithNbpObservableError() { + NbpObservable c = normal.completable + .startWith(NbpObservable.error(new TestException())); + + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(0); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test(expected = NullPointerException.class) + public void startWithCompletableNull() { + normal.completable.startWith((Completable)null); + } + + @Test(expected = NullPointerException.class) + public void startWithFlowableNull() { + normal.completable.startWith((Observable)null); + } + + @Test(expected = NullPointerException.class) + public void startWithNbpObservableNull() { + normal.completable.startWith((NbpObservable)null); + } + + @Test(expected = NullPointerException.class) + public void endWithCompletableNull() { + normal.completable.endWith((Completable)null); + } + + @Test(expected = NullPointerException.class) + public void endWithFlowableNull() { + normal.completable.endWith((Observable)null); + } + + @Test(expected = NullPointerException.class) + public void endWithNbpObservableNull() { + normal.completable.endWith((NbpObservable)null); + } + + @Test(timeout = 1000) + public void endWithCompletableNormal() { + AtomicBoolean run = new AtomicBoolean(); + Completable c = normal.completable + .endWith(Completable.fromCallable(() -> { + run.set(normal.get() == 0); + return null; + })); + + c.await(); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + } + + @Test(timeout = 1000) + public void endWithCompletableError() { + Completable c = normal.completable.endWith(error.completable); + + try { + c.await(); + Assert.fail("Did not throw TestException"); + } catch (TestException ex) { + normal.assertSubscriptions(1); + error.assertSubscriptions(1); + } + } + + @Test(timeout = 1000) + public void endWithFlowableNormal() { + AtomicBoolean run = new AtomicBoolean(); + Observable c = normal.completable + .endWith(Observable.fromCallable(() -> { + run.set(normal.get() == 0); + return 1; + })); + + TestSubscriber ts = new TestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void endWithFlowableError() { + Observable c = normal.completable + .endWith(Observable.error(new TestException())); + + TestSubscriber ts = new TestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(1); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } + + @Test(timeout = 1000) + public void endWithNbpObservableNormal() { + AtomicBoolean run = new AtomicBoolean(); + NbpObservable c = normal.completable + .endWith(NbpObservable.fromCallable(() -> { + run.set(normal.get() == 0); + return 1; + })); + + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + c.subscribe(ts); + + Assert.assertFalse("Start with other", run.get()); + normal.assertSubscriptions(1); + + ts.assertValue(1); + ts.assertComplete(); + ts.assertNoErrors(); + } + + @Test(timeout = 1000) + public void endWithNbpObservableError() { + NbpObservable c = normal.completable + .endWith(NbpObservable.error(new TestException())); + + NbpTestSubscriber ts = new NbpTestSubscriber<>(); + + c.subscribe(ts); + + normal.assertSubscriptions(1); + + ts.assertNoValues(); + ts.assertError(TestException.class); + ts.assertNotComplete(); + } +}