diff --git a/.gitignore b/.gitignore index b2347058..70c8a5a2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ test-results test-tmp *.class gradle.properties +*.orig diff --git a/.java-version b/.java-version new file mode 100644 index 00000000..ec635144 --- /dev/null +++ b/.java-version @@ -0,0 +1 @@ +9 diff --git a/.travis.yml b/.travis.yml index f987c6f1..4e945266 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,34 @@ language: java +sudo: required +dist: trusty +#group: edge + script: - ./gradlew check cache: directories: - $HOME/.gradle -jdk: - - openjdk6 + +before_install: + - export GRADLE_OPTS=-Xmx1024m + +matrix: + include: + - jdk: openjdk7 + - jdk: oraclejdk8 # JDK 1.8.0_131-b11 + - jdk: oraclejdk9 + +# Don't let Travis CI execute './gradlew assemble' by default +# From https://github.com/reactive-streams/reactive-streams-jvm/pull/383 +install: +# Display Gradle, JVM and other versions + - ./gradlew -version + env: global: - TERM=dumb - DEFAULT_TIMEOUT_MILLIS=300 + - DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS=200 - PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS=300 +addons: # Fix OpenJDK build. Issue: https://github.com/travis-ci/travis-ci/issues/5227 & https://docs.travis-ci.com/user/hostname + hostname: rs-jvm diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 56c03acd..e18c4350 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,7 +4,7 @@ The Reactive Streams project welcomes contributions from anybody who wants to pa ## Copyright Statement -The aforementioned waiver of copyrights and other rights is represented by the addition of a line to the file [CopyrightWaivers.txt](https://github.com/reactive-streams/reactive-streams/blob/master/CopyrightWaivers.txt). For a pull request to be considered every contributor must have signed the copyright statement in this way; this may be included within that same pull request. +The aforementioned waiver of copyrights and other rights is represented by the addition of a line to the file [CopyrightWaivers.txt](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/CopyrightWaivers.txt). For a pull request to be considered every contributor must have signed the copyright statement in this way; this may be included within that same pull request. ## Gatekeepers diff --git a/CopyrightWaivers.txt b/CopyrightWaivers.txt index 316ea33c..596260dc 100644 --- a/CopyrightWaivers.txt +++ b/CopyrightWaivers.txt @@ -12,7 +12,7 @@ waiver with respect to the entirety of my contributions. The text of the copyright statement is included in the COPYING file at the root of the reactive-streams repository at -https://github.com/reactive-streams/reactive-streams/blob/master/COPYING. +https://github.com/reactive-streams/reactive-streams-jvm/blob/master/COPYING. Underwriting parties: @@ -28,3 +28,14 @@ ouertani | Slim Ouertani, ouertani@gmail.com 2m | Martynas Mickevičius, mmartynas@gmail.com, Typesafe Inc. ldaley | Luke Daley, luke.daley@gradleware.com, Gradleware Inc. colinrgodsey | Colin Godsey, crgodsey@gmail.com, MediaMath Inc. +davidmoten | Dave Moten, davidmoten@gmail.com +briantopping | Brian Topping, brian.topping@gmail.com, Mauswerks LLC +rstoyanchev | Rossen Stoyanchev, rstoyanchev@pivotal.io, Pivotal +BjornHamels | Björn Hamels, bjorn@hamels.nl +JakeWharton | Jake Wharton, jakewharton@gmail.com +anthonyvdotbe | Anthony Vanelverdinghe, anthonyv.be@outlook.com +seratch | Kazuhiro Sera, seratch@gmail.com, SmartNews, Inc. +akarnokd | David Karnok, akarnokd@gmail.com +egetman | Evgeniy Getman, getman.eugene@gmail.com +patriknw | Patrik Nordwall, patrik.nordwall@gmail.com, Lightbend Inc +angelsanz | Ángel Sanz, angelsanz@users.noreply.github.com diff --git a/README.md b/README.md index ca4b94b2..1cd1768c 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,18 @@ The purpose of Reactive Streams is to provide a standard for asynchronous stream processing with non-blocking backpressure. -The latest preview release is available on Maven Central as +The latest release is available on Maven Central as ```xml org.reactivestreams reactive-streams - 1.0.0.RC1 + 1.0.1 org.reactivestreams reactive-streams-tck - 1.0.0.RC1 + 1.0.1 test ``` @@ -22,11 +22,11 @@ The latest preview release is available on Maven Central as Handling streams of data—especially “live” data whose volume is not predetermined—requires special care in an asynchronous system. The most prominent issue is that resource consumption needs to be carefully controlled such that a fast data source does not overwhelm the stream destination. Asynchrony is needed in order to enable the parallel use of computing resources, on collaborating network hosts or multiple CPU cores within a single machine. -The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundary – think passing elements on to another thread or thread-pool — while ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, backpressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded. The benefits of asynchronous processing would be negated if the communication of backpressure were synchronous (see also the [Reactive Manifesto](http://reactivemanifesto.org/)), therefore care has been taken to mandate fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation. +The main goal of Reactive Streams is to govern the exchange of stream data across an asynchronous boundary – think passing elements on to another thread or thread-pool — while ensuring that the receiving side is not forced to buffer arbitrary amounts of data. In other words, backpressure is an integral part of this model in order to allow the queues which mediate between threads to be bounded. The benefits of asynchronous processing would be negated if the backpressure signals were synchronous (see also the [Reactive Manifesto](http://reactivemanifesto.org/)), therefore care has been taken to mandate fully non-blocking and asynchronous behavior of all aspects of a Reactive Streams implementation. It is the intention of this specification to allow the creation of many conforming implementations, which by virtue of abiding by the rules will be able to interoperate smoothly, preserving the aforementioned benefits and characteristics across the whole processing graph of a stream application. -It should be noted that the precise nature of stream manipulations (transformation, splitting, merging, etc.) is not covered by this specification. Reactive Streams are only concerned with mediating the stream of data between different processing elements. In their development care has been taken to ensure that all basic ways of combining streams can be expressed. +It should be noted that the precise nature of stream manipulations (transformation, splitting, merging, etc.) is not covered by this specification. Reactive Streams are only concerned with mediating the stream of data between different [API Components](#api-components). In their development care has been taken to ensure that all basic ways of combining streams can be expressed. In summary, Reactive Streams is a standard and specification for Stream-oriented libraries for the JVM that @@ -57,18 +57,33 @@ A *Publisher* is a provider of a potentially unbounded number of sequenced eleme In response to a call to `Publisher.subscribe(Subscriber)` the possible invocation sequences for methods on the `Subscriber` are given by the following protocol: ``` -onError | (onSubscribe onNext* (onError | onComplete)?) +onSubscribe onNext* (onError | onComplete)? ``` +This means that `onSubscribe` is always signalled, +followed by a possibly unbounded number of `onNext` signals (as requested by `Subscriber`) followed by an `onError` signal if there is a failure, or an `onComplete` signal when no more elements are available—all as long as the `Subscription` is not cancelled. + #### NOTES - The specifications below use binding words in capital letters from https://www.ietf.org/rfc/rfc2119.txt -- The terms `emit`, `signal` or `send` are interchangeable. The specifications below will use `signal`. -- The terms `synchronously` or `synchronous` refer to executing in the calling `Thread`. + +### Glossary + +| Term | Definition | +| ------------------------- | ------------------------------------------------------------------------------------------------------ | +| Signal | As a noun: one of the `onSubscribe`, `onNext`, `onComplete`, `onError`, `request(n)` or `cancel` methods. As a verb: calling/invoking a signal. | +| Demand | As a noun, the aggregated number of elements requested by a Subscriber which is yet to be delivered (fulfilled) by the Publisher. As a verb, the act of `request`-ing more elements. | +| Synchronous(ly) | Executes on the calling Thread. | +| Return normally | Only ever returns a value of the declared type to the caller. The only legal way to signal failure to a `Subscriber` is via the `onError` method.| +| Responsivity | Readiness/ability to respond. In this document used to indicate that the different components should not impair each others ability to respond. | +| Non-obstructing | Quality describing a method which is as quick to execute as possible—on the calling thread. This means, for example, avoids heavy computations and other things that would stall the caller´s thread of execution. | +| Terminal state | For a Publisher: When `onComplete` or `onError` has been signalled. For a Subscriber: When an `onComplete` or `onError` has been received.| +| NOP | Execution that has no detectable effect to the calling thread, and can as such safely be called any number of times.| +| External synchronization | Access coordination for thread safety purposes implemented outside of the constructs defined in this specification, using techniques such as, but not limited to, `atomics`, `monitors`, or `locks`. | ### SPECIFICATION -#### 1. Publisher ([Code](https://github.com/reactive-streams/reactive-streams/blob/v1.0.0.RC1/api/src/main/java/org/reactivestreams/Publisher.java)) +#### 1. Publisher ([Code](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/api/src/main/java/org/reactivestreams/Publisher.java)) ```java public interface Publisher { @@ -76,25 +91,32 @@ public interface Publisher { } ```` -| ID | Rule . | -| ------------------------- | ------------------------------------------------------------------------------------------------------. | -| 1 | The total number of `onNext` signals sent by a `Publisher` to a `Subscriber` MUST be less than or equal to the total number of elements requested by that `Subscriber`´s `Subscription` at all times. | -| 2 | A `Publisher` MAY signal less `onNext` than requested and terminate the `Subscription` by calling `onComplete` or `onError`. | -| 3 | `onSubscribe`, `onNext`, `onError` and `onComplete` signaled to a `Subscriber` MUST be signaled sequentially (no concurrent notifications). | -| 4 | If a `Publisher` fails it MUST signal an `onError`. | -| 5 | If a `Publisher` terminates successfully (finite stream) it MUST signal an `onComplete`. | -| 6 | If a `Publisher` signals either `onError` or `onComplete` on a `Subscriber`, that `Subscriber`’s `Subscription` MUST be considered cancelled. | -| 7 | Once a terminal state has been signaled (`onError`, `onComplete`) it is REQUIRED that no further signals occur. | -| 8 | If a `Subscription` is cancelled its `Subscriber` MUST eventually stop being signaled. | -| 9 | Calling `Publisher.subscribe` MUST return normally. The only legal way to signal failure (or reject a `Subscriber`) is via the `onError` method. | -| 10 | `Publisher.subscribe` MAY be called as many times as wanted but MUST be with a different `Subscriber` each time [see [2.12](#2.12)]. | -| 11 | A `Publisher` MAY support multi-subscribe and choose whether each `Subscription` is unicast or multicast. | -| 12 | A `Publisher` MAY reject calls to its `subscribe` method if it is unable or unwilling to serve them [[1](#footnote-1-1)]. If rejecting it MUST do this by calling `onError` on the `Subscriber` passed to `Publisher.subscribe` instead of calling `onSubscribe`. | -| 13 | A `Publisher` MUST produce the same elements, starting with the oldest element still available, in the same sequence for all its subscribers and MAY produce the stream elements at (temporarily) differing rates to different subscribers. | - -[1] : A stateful Publisher can be overwhelmed, bounded by a finite number of underlying resources, exhausted, shut-down or in a failed state. - -#### 2. Subscriber ([Code](https://github.com/reactive-streams/reactive-streams/blob/v1.0.0.RC1/api/src/main/java/org/reactivestreams/Subscriber.java)) +| ID | Rule | +| ------------------------- | ------------------------------------------------------------------------------------------------------ | +| 1 | The total number of `onNext`´s signalled by a `Publisher` to a `Subscriber` MUST be less than or equal to the total number of elements requested by that `Subscriber`´s `Subscription` at all times. | +| [:bulb:](#1.1 "1.1 explained") | *The intent of this rule is to make it clear that Publishers cannot signal more elements than Subscribers have requested. There’s an implicit, but important, consequence to this rule: Since demand can only be fulfilled after it has been received, there’s a happens-before relationship between requesting elements and receiving elements.* | +| 2 | A `Publisher` MAY signal fewer `onNext` than requested and terminate the `Subscription` by calling `onComplete` or `onError`. | +| [:bulb:](#1.2 "1.2 explained") | *The intent of this rule is to make it clear that a Publisher cannot guarantee that it will be able to produce the number of elements requested; it simply might not be able to produce them all; it may be in a failed state; it may be empty or otherwise already completed.* | +| 3 | `onSubscribe`, `onNext`, `onError` and `onComplete` signaled to a `Subscriber` MUST be signaled in a `thread-safe` manner—and if performed by multiple threads—use [external synchronization](#term_ext_sync). | +| [:bulb:](#1.3 "1.3 explained") | *The intent of this rule is to make it clear that [external synchronization](#term_ext_sync) must be employed if the Publisher intends to send signals from multiple/different threads.* | +| 4 | If a `Publisher` fails it MUST signal an `onError`. | +| [:bulb:](#1.4 "1.4 explained") | *The intent of this rule is to make it clear that a Publisher is responsible for notifying its Subscribers if it detects that it cannot proceed—Subscribers must be given a chance to clean up resources or otherwise deal with the Publisher´s failures.* | +| 5 | If a `Publisher` terminates successfully (finite stream) it MUST signal an `onComplete`. | +| [:bulb:](#1.5 "1.5 explained") | *The intent of this rule is to make it clear that a Publisher is responsible for notifying its Subscribers that it has reached a [terminal state](#term_terminal_state)—Subscribers can then act on this information; clean up resources, etc.* | +| 6 | If a `Publisher` signals either `onError` or `onComplete` on a `Subscriber`, that `Subscriber`’s `Subscription` MUST be considered cancelled. | +| [:bulb:](#1.6 "1.6 explained") | *The intent of this rule is to make sure that a Subscription is treated the same no matter if it was cancelled, the Publisher signalled onError or onComplete.* | +| 7 | Once a [terminal state](#term_terminal_state) has been signaled (`onError`, `onComplete`) it is REQUIRED that no further signals occur. | +| [:bulb:](#1.7 "1.7 explained") | *The intent of this rule is to make sure that onError and onComplete are the final states of an interaction between a Publisher and Subscriber pair.* | +| 8 | If a `Subscription` is cancelled its `Subscriber` MUST eventually stop being signaled. | +| [:bulb:](#1.8 "1.8 explained") | *The intent of this rule is to make sure that Publishers respect a Subscriber’s request to cancel a Subscription when Subscription.cancel() has been called. The reason for *eventually* is because signals can have propagation delay due to being asynchronous.* | +| 9 | `Publisher.subscribe` MUST call `onSubscribe` on the provided `Subscriber` prior to any other signals to that `Subscriber` and MUST [return normally](#term_return_normally), except when the provided `Subscriber` is `null` in which case it MUST throw a `java.lang.NullPointerException` to the caller, for all other situations the only legal way to signal failure (or reject the `Subscriber`) is by calling `onError` (after calling `onSubscribe`). | +| [:bulb:](#1.9 "1.9 explained") | *The intent of this rule is to make sure that `onSubscribe` is always signalled before any of the other signals, so that initialization logic can be executed by the Subscriber when the signal is received. Also `onSubscribe` MUST only be called at most once, [see [2.12](#2.12)]. If the supplied `Subscriber` is `null`, there is nowhere else to signal this but to the caller, which means a `java.lang.NullPointerException` must be thrown. Examples of possible situations: A stateful Publisher can be overwhelmed, bounded by a finite number of underlying resources, exhausted, or in a [terminal state](#term_terminal_state).* | +| 10 | `Publisher.subscribe` MAY be called as many times as wanted but MUST be with a different `Subscriber` each time [see [2.12](#2.12)]. | +| [:bulb:](#1.10 "1.10 explained") | *The intent of this rule is to have callers of `subscribe` be aware that a generic Publisher and a generic Subscriber cannot be assumed to support being attached multiple times. Furthermore, it also mandates that the semantics of `subscribe` must be upheld no matter how many times it is called.* | +| 11 | A `Publisher` MAY support multiple `Subscriber`s and decides whether each `Subscription` is unicast or multicast. | +| [:bulb:](#1.11 "1.11 explained") | *The intent of this rule is to give Publisher implementations the flexibility to decide how many, if any, Subscribers they will support, and how elements are going to be distributed.* | + +#### 2. Subscriber ([Code](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/api/src/main/java/org/reactivestreams/Subscriber.java)) ```java public interface Subscriber { @@ -105,25 +127,36 @@ public interface Subscriber { } ```` -| ID | Rule . | -| ------------------------- | ------------------------------------------------------------------------------------------------------. | -| 1 | A `Subscriber` MUST signal demand via `Subscription.request(long n)` to receive `onNext` signals. | -| 2 | If a `Subscriber` suspects that its processing of signals will negatively impact its `Publisher`'s responsivity, it is RECOMMENDED that it asynchronously dispatches its signals. | -| 3 | `Subscriber.onComplete()` and `Subscriber.onError(Throwable t)` MUST NOT call any methods on the `Subscription` or the `Publisher`. | -| 4 | `Subscriber.onComplete()` and `Subscriber.onError(Throwable t)` MUST consider the Subscription cancelled after having received the signal. | -| 5 | A `Subscriber` MUST call `Subscription.cancel()` on the given `Subscription` after an `onSubscribe` signal if it already has an active `Subscription`. | -| 6 | A `Subscriber` MUST call `Subscription.cancel()` if it is no longer valid to the `Publisher` without the `Publisher` having signaled `onError` or `onComplete`. | -| 7 | A `Subscriber` MUST ensure that all calls on its `Subscription` take place from the same thread or provide for respective external synchronization. | -| 8 | A `Subscriber` MUST be prepared to receive one or more `onNext` signals after having called `Subscription.cancel()` if there are still requested elements pending [see [3.12](#3.12)]. `Subscription.cancel()` does not guarantee to perform the underlying cleaning operations immediately. | -| 9 | A `Subscriber` MUST be prepared to receive an `onComplete` signal with or without a preceding `Subscription.request(long n)` call. | -| 10 | A `Subscriber` MUST be prepared to receive an `onError` signal with or without a preceding `Subscription.request(long n)` call. | -| 11 | A `Subscriber` MUST make sure that all calls on its `onXXX` methods happen-before [[1]](#footnote-2-1) the processing of the respective signals. I.e. the Subscriber must take care of properly publishing the signal to its processing logic. | -| 12 | `Subscriber.onSubscribe` MUST be called at most once for a given `Subscriber` (based on object equality). | -| 13 | Calling `onSubscribe`, `onNext`, `onError` or `onComplete` MUST return normally. The only legal way for a `Subscriber` to signal failure is by cancelling its `Subscription`. In the case that this rule is violated, any associated `Subscription` to the `Subscriber` MUST be considered as cancelled, and the caller MUST raise this error condition in a fashion that is adequate for the runtime environment. | - -[1] : See JMM definition of Happen-Before in section 17.4.5. on http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html - -#### 3. Subscription ([Code](https://github.com/reactive-streams/reactive-streams/blob/v1.0.0.RC1/api/src/main/java/org/reactivestreams/Subscription.java)) +| ID | Rule | +| ------------------------- | ------------------------------------------------------------------------------------------------------ | +| 1 | A `Subscriber` MUST signal demand via `Subscription.request(long n)` to receive `onNext` signals. | +| [:bulb:](#2.1 "2.1 explained") | *The intent of this rule is to establish that it is the responsibility of the Subscriber to signal when, and how many, elements it is able and willing to receive.* | +| 2 | If a `Subscriber` suspects that its processing of signals will negatively impact its `Publisher`´s responsivity, it is RECOMMENDED that it asynchronously dispatches its signals. | +| [:bulb:](#2.2 "2.2 explained") | *The intent of this rule is that a Subscriber should [not obstruct](#term_non-obstructing) the progress of the Publisher from an execution point-of-view. In other words, the Subscriber should not starve the Publisher from CPU cycles.* | +| 3 | `Subscriber.onComplete()` and `Subscriber.onError(Throwable t)` MUST NOT call any methods on the `Subscription` or the `Publisher`. | +| [:bulb:](#2.3 "2.3 explained") | *The intent of this rule is to prevent cycles and race-conditions—between Publisher, Subscription and Subscriber—during the processing of completion signals.* | +| 4 | `Subscriber.onComplete()` and `Subscriber.onError(Throwable t)` MUST consider the Subscription cancelled after having received the signal. | +| [:bulb:](#2.4 "2.4 explained") | *The intent of this rule is to make sure that Subscribers respect a Publisher’s [terminal state](#term_terminal_state) signals. A Subscription is simply not valid anymore after an onComplete or onError signal has been received.* | +| 5 | A `Subscriber` MUST call `Subscription.cancel()` on the given `Subscription` after an `onSubscribe` signal if it already has an active `Subscription`. | +| [:bulb:](#2.5 "2.5 explained") | *The intent of this rule is to prevent that two, or more, separate Publishers from thinking that they can interact with the same Subscriber. Enforcing this rule means that resource leaks are prevented since extra Subscriptions will be cancelled.* | +| 6 | A `Subscriber` MUST call `Subscription.cancel()` if the `Subscription` is no longer needed. | +| [:bulb:](#2.6 "2.6 explained") | *The intent of this rule is to establish that Subscribers cannot just throw Subscriptions away when they are no longer needed, they have to call `cancel` so that resources held by that Subscription can be safely, and timely, reclaimed. An example of this would be a Subscriber which is only interested in a specific element, which would then cancel its Subscription to signal its completion to the Publisher.* | +| 7 | A `Subscriber` MUST ensure that all calls on its `Subscription` take place from the same thread or provide for respective [external synchronization](#term_ext_sync). | +| [:bulb:](#2.7 "2.7 explained") | *The intent of this rule is to establish that [external synchronization](#term_ext_sync) must be added if a Subscriber will be using a Subscription concurrently by two or more threads.* | +| 8 | A `Subscriber` MUST be prepared to receive one or more `onNext` signals after having called `Subscription.cancel()` if there are still requested elements pending [see [3.12](#3.12)]. `Subscription.cancel()` does not guarantee to perform the underlying cleaning operations immediately. | +| [:bulb:](#2.8 "2.8 explained") | *The intent of this rule is to highlight that there may be a delay between calling `cancel` the Publisher seeing that.* | +| 9 | A `Subscriber` MUST be prepared to receive an `onComplete` signal with or without a preceding `Subscription.request(long n)` call. | +| [:bulb:](#2.9 "2.9 explained") | *The intent of this rule is to establish that completion is unrelated to the demand flow—this allows for streams which complete early, and obviates the need to *poll* for completion.* | +| 10 | A `Subscriber` MUST be prepared to receive an `onError` signal with or without a preceding `Subscription.request(long n)` call. | +| [:bulb:](#2.10 "2.10 explained") | *The intent of this rule is to establish that Publisher failures may be completely unrelated to signalled demand. This means that Subscribers do not need to poll to find out if the Publisher will not be able to fulfill its requests.* | +| 11 | A `Subscriber` MUST make sure that all calls on its [signal](#term_signal) methods happen-before the processing of the respective signals. I.e. the Subscriber must take care of properly publishing the signal to its processing logic. | +| [:bulb:](#2.11 "2.11 explained") | *The intent of this rule is to establish that it is the responsibility of the Subscriber implementation to make sure that asynchronous processing of its signals are thread safe. See [JMM definition of Happens-Before in section 17.4.5](https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.4.5).* | +| 12 | `Subscriber.onSubscribe` MUST be called at most once for a given `Subscriber` (based on object equality). | +| [:bulb:](#2.12 "2.12 explained") | *The intent of this rule is to establish that it MUST be assumed that the same Subscriber can only be subscribed at most once. Note that `object equality` is `a.equals(b)`.* | +| 13 | Calling `onSubscribe`, `onNext`, `onError` or `onComplete` MUST [return normally](#term_return_normally) except when any provided parameter is `null` in which case it MUST throw a `java.lang.NullPointerException` to the caller, for all other situations the only legal way for a `Subscriber` to signal failure is by cancelling its `Subscription`. In the case that this rule is violated, any associated `Subscription` to the `Subscriber` MUST be considered as cancelled, and the caller MUST raise this error condition in a fashion that is adequate for the runtime environment. | +| [:bulb:](#2.13 "2.13 explained") | *The intent of this rule is to establish the semantics for the methods of Subscriber and what the Publisher is allowed to do in which case this rule is violated. «Raise this error condition in a fashion that is adequate for the runtime environment» could mean logging the error—or otherwise make someone or something aware of the situation—as the error cannot be signalled to the faulty Subscriber.* | + +#### 3. Subscription ([Code](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/api/src/main/java/org/reactivestreams/Subscription.java)) ```java public interface Subscription { @@ -132,45 +165,58 @@ public interface Subscription { } ```` -| ID | Rule . | -| ------------------------- | ------------------------------------------------------------------------------------------------------. | -| 1 | `Subscription.request` and `Subscription.cancel` MUST only be called inside of its `Subscriber` context. A `Subscription` represents the unique relationship between a `Subscriber` and a `Publisher` [see [2.12](#2.12)]. | -| 2 | The `Subscription` MUST allow the `Subscriber` to call `Subscription.request` synchronously from within `onNext` or `onSubscribe`. | -| 3 | `Subscription.request` MUST place an upper bound on possible synchronous recursion between `Publisher` and `Subscriber`[[1](#footnote-3-1)]. | -| 4 | `Subscription.request` SHOULD respect the responsivity of its caller by returning in a timely manner[[2](#footnote-3-2)]. | -| 5 | `Subscription.cancel` MUST respect the responsivity of its caller by returning in a timely manner[[2](#footnote-3-2)], MUST be idempotent and MUST be thread-safe. | -| 6 | After the `Subscription` is cancelled, additional `Subscription.request(long n)` MUST be NOPs. | -| 7 | After the `Subscription` is cancelled, additional `Subscription.cancel()` MUST be NOPs. | -| 8 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MUST register the given number of additional elements to be produced to the respective subscriber. | -| 9 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MUST signal `onError` with a `java.lang.IllegalArgumentException` if the argument is <= 0. The cause message MUST include a reference to this rule and/or quote the full rule. | -| 10 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MAY synchronously call `onNext` on this (or other) subscriber(s). | -| 11 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MAY synchronously call `onComplete` or `onError` on this (or other) subscriber(s). | +| ID | Rule | +| ------------------------- | ------------------------------------------------------------------------------------------------------ | +| 1 | `Subscription.request` and `Subscription.cancel` MUST only be called inside of its `Subscriber` context. | +| [:bulb:](#3.1 "3.1 explained") | *The intent of this rule is to establish that a Subscription represents the unique relationship between a Subscriber and a Publisher [see [2.12](#2.12)]. The Subscriber is in control over when elements are requested and when more elements are no longer needed.* | +| 2 | The `Subscription` MUST allow the `Subscriber` to call `Subscription.request` synchronously from within `onNext` or `onSubscribe`. | +| [:bulb:](#3.2 "3.2 explained") | *The intent of this rule is to make it clear that implementations of `request` must be reentrant, to avoid stack overflows in the case of mutual recursion between `request` and `onNext` (and eventually `onComplete` / `onError`). This implies that Publishers can be `synchronous`, i.e. signalling `onNext`´s on the thread which calls `request`.* | +| 3 | `Subscription.request` MUST place an upper bound on possible synchronous recursion between `Publisher` and `Subscriber`. | +| [:bulb:](#3.3 "3.3 explained") | *The intent of this rule is to complement [see [3.2](#3.2)] by placing an upper limit on the mutual recursion between `request` and `onNext` (and eventually `onComplete` / `onError`). Implementations are RECOMMENDED to limit this mutual recursion to a depth of `1` (ONE)—for the sake of conserving stack space. An example for undesirable synchronous, open recursion would be Subscriber.onNext -> Subscription.request -> Subscriber.onNext -> …, as it otherwise will result in blowing the calling Thread´s stack.* | +| 4 | `Subscription.request` SHOULD respect the responsivity of its caller by returning in a timely manner. | +| [:bulb:](#3.4 "3.4 explained") | *The intent of this rule is to establish that `request` is intended to be a [non-obstructing](#term_non-obstructing) method, and should be as quick to execute as possible on the calling thread, so avoid heavy computations and other things that would stall the caller´s thread of execution.* | +| 5 | `Subscription.cancel` MUST respect the responsivity of its caller by returning in a timely manner, MUST be idempotent and MUST be thread-safe. | +| [:bulb:](#3.5 "3.5 explained") | *The intent of this rule is to establish that `cancel` is intended to be a [non-obstructing](#term_non-obstructing) method, and should be as quick to execute as possible on the calling thread, so avoid heavy computations and other things that would stall the caller´s thread of execution. Furthermore, it is also important that it is possible to call it multiple times without any adverse effects.* | +| 6 | After the `Subscription` is cancelled, additional `Subscription.request(long n)` MUST be [NOPs](#term_nop). | +| [:bulb:](#3.6 "3.6 explained") | *The intent of this rule is to establish a causal relationship between cancellation of a subscription and the subsequent non-operation of requesting more elements.* | +| 7 | After the `Subscription` is cancelled, additional `Subscription.cancel()` MUST be [NOPs](#term_nop). | +| [:bulb:](#3.7 "3.7 explained") | *The intent of this rule is superseded by [3.5](#3.5).* | +| 8 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MUST register the given number of additional elements to be produced to the respective subscriber. | +| [:bulb:](#3.8 "3.8 explained") | *The intent of this rule is to make sure that `request`-ing is an additive operation, as well as ensuring that a request for elements is delivered to the Publisher.* | +| 9 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MUST signal `onError` with a `java.lang.IllegalArgumentException` if the argument is <= 0. The cause message SHOULD explain that non-positive request signals are illegal. | +| [:bulb:](#3.9 "3.9 explained") | *The intent of this rule is to prevent faulty implementations to proceed operation without any exceptions being raised. Requesting a negative or 0 number of elements, since requests are additive, most likely to be the result of an erroneous calculation on the behalf of the Subscriber.* | +| 10 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MAY synchronously call `onNext` on this (or other) subscriber(s). | +| [:bulb:](#3.10 "3.10 explained") | *The intent of this rule is to establish that it is allowed to create synchronous Publishers, i.e. Publishers who execute their logic on the calling thread.* | +| 11 | While the `Subscription` is not cancelled, `Subscription.request(long n)` MAY synchronously call `onComplete` or `onError` on this (or other) subscriber(s). | +| [:bulb:](#3.11 "3.11 explained") | *The intent of this rule is to establish that it is allowed to create synchronous Publishers, i.e. Publishers who execute their logic on the calling thread.* | | 12 | While the `Subscription` is not cancelled, `Subscription.cancel()` MUST request the `Publisher` to eventually stop signaling its `Subscriber`. The operation is NOT REQUIRED to affect the `Subscription` immediately. | -| 13 | While the `Subscription` is not cancelled, `Subscription.cancel()` MUST request the `Publisher` to eventually drop any references to the corresponding subscriber. Re-subscribing with the same `Subscriber` object is discouraged [see [2.12](#2.12)], but this specification does not mandate that it is disallowed since that would mean having to store previously cancelled subscriptions indefinitely. | -| 14 | While the `Subscription` is not cancelled, calling `Subscription.cancel` MAY cause the `Publisher`, if stateful, to transition into the `shut-down` state if no other `Subscription` exists at this point [see [1.13](#1.13)]. -| 15 | Calling `Subscription.cancel` MUST return normally. The only legal way to signal failure to a `Subscriber` is via the `onError` method. | -| 16 | Calling `Subscription.request` MUST return normally. The only legal way to signal failure to a `Subscriber` is via the `onError` method. | -| 17 | A `Subscription` MUST support an unbounded number of calls to request and MUST support a demand (sum requested - sum delivered) up to 2^63-1 (`java.lang.Long.MAX_VALUE`). A demand equal or greater than 2^63-1 (`java.lang.Long.MAX_VALUE`) MAY be considered by the `Publisher` as “effectively unbounded”[[1](#footnote-3-1)]. | - -[1] : An example for undesirable synchronous, open recursion would be `Subscriber.onNext` -> `Subscription.request` -> `Subscriber.onNext` -> …, as it very quickly would result in blowing the calling Thread´s stack. - -[2] : Avoid heavy computations and other things that would stall the caller´s thread of execution - -[3] : As it is not feasibly reachable with current or foreseen hardware within a reasonable amount of time (1 element per nanosecond would take 292 years) to fulfill a demand of 2^63-1, it is allowed for a `Publisher` to stop tracking demand beyond this point. +| [:bulb:](#3.12 "3.12 explained") | *The intent of this rule is to establish that the desire to cancel a Subscription is eventually respected by the Publisher, acknowledging that it may take some time before the signal is received.* | +| 13 | While the `Subscription` is not cancelled, `Subscription.cancel()` MUST request the `Publisher` to eventually drop any references to the corresponding subscriber. | +| [:bulb:](#3.13 "3.13 explained") | *The intent of this rule is to make sure that Subscribers can be properly garbage-collected after their subscription no longer being valid. Re-subscribing with the same Subscriber object is discouraged [see [2.12](#2.12)], but this specification does not mandate that it is disallowed since that would mean having to store previously cancelled subscriptions indefinitely.* | +| 14 | While the `Subscription` is not cancelled, calling `Subscription.cancel` MAY cause the `Publisher`, if stateful, to transition into the `shut-down` state if no other `Subscription` exists at this point [see [1.9](#1.9)]. | +| [:bulb:](#3.14 "3.14 explained") | *The intent of this rule is to allow for Publishers to signal `onComplete` or `onError` following `onSubscribe` for new Subscribers in response to a cancellation signal from an existing Subscriber.* | +| 15 | Calling `Subscription.cancel` MUST [return normally](#term_return_normally). | +| [:bulb:](#3.15 "3.15 explained") | *The intent of this rule is to disallow implementations to throw exceptions in response to `cancel` being called.* | +| 16 | Calling `Subscription.request` MUST [return normally](#term_return_normally). | +| [:bulb:](#3.16 "3.16 explained") | *The intent of this rule is to disallow implementations to throw exceptions in response to `request` being called.* | +| 17 | A `Subscription` MUST support an unbounded number of calls to `request` and MUST support a demand up to 2^63-1 (`java.lang.Long.MAX_VALUE`). A demand equal or greater than 2^63-1 (`java.lang.Long.MAX_VALUE`) MAY be considered by the `Publisher` as “effectively unbounded”. | +| [:bulb:](#3.17 "3.17 explained") | *The intent of this rule is to establish that the Subscriber can request an unbounded number of elements, in any increment above 0 [see [3.9](#3.9)], in any number of invocations of `request`. As it is not feasibly reachable with current or foreseen hardware within a reasonable amount of time (1 element per nanosecond would take 292 years) to fulfill a demand of 2^63-1, it is allowed for a Publisher to stop tracking demand beyond this point.* | A `Subscription` is shared by exactly one `Publisher` and one `Subscriber` for the purpose of mediating the data exchange between this pair. This is the reason why the `subscribe()` method does not return the created `Subscription`, but instead returns `void`; the `Subscription` is only passed to the `Subscriber` via the `onSubscribe` callback. -#### 4.Processor ([Code](https://github.com/reactive-streams/reactive-streams/blob/v1.0.0.RC1/api/src/main/java/org/reactivestreams/Processor.java)) +#### 4.Processor ([Code](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/api/src/main/java/org/reactivestreams/Processor.java)) ```java public interface Processor extends Subscriber, Publisher { } ```` -| ID | Rule . | -| ------------------------ | ------------------------------------------------------------------------------------------------------. | +| ID | Rule | +| ------------------------ | ------------------------------------------------------------------------------------------------------ | | 1 | A `Processor` represents a processing stage—which is both a `Subscriber` and a `Publisher` and MUST obey the contracts of both. | +| [:bulb:](#4.1 "4.1 explained") | *The intent of this rule is to establish that Processors behave, and are bound by, both the Publisher and Subscriber specifications.* | | 2 | A `Processor` MAY choose to recover an `onError` signal. If it chooses to do so, it MUST consider the `Subscription` cancelled, otherwise it MUST propagate the `onError` signal to its Subscribers immediately. | +| [:bulb:](#4.2 "4.2 explained") | *The intent of this rule is to inform that it’s possible for implementations to be more than simple transformations.* | While not mandated, it can be a good idea to cancel a `Processors` upstream `Subscription` when/if its last `Subscriber` cancels their `Subscription`, to let the cancellation signal propagate upstream. @@ -185,7 +231,7 @@ Take this example: nioSelectorThreadOrigin map(f) filter(p) consumeTo(toNioSelectorOutput) ``` -It has an async origin and an async destination. Let's assume that both origin and destination are selector event loops. The `Subscription.request(n)` must be chained from the destination to the origin. This is now where each implementation can choose how to do this. +It has an async origin and an async destination. Let’s assume that both origin and destination are selector event loops. The `Subscription.request(n)` must be chained from the destination to the origin. This is now where each implementation can choose how to do this. The following uses the pipe `|` character to signal async boundaries (queue and schedule) and `R#` to represent resources (possibly threads). @@ -212,7 +258,7 @@ nioSelectorThreadOrigin | map(f) filter(p) consumeTo(toNioSelectorOutput) All of these variants are "asynchronous streams". They all have their place and each has different tradeoffs including performance and implementation complexity. -The Reactive Streams contract allows implementations the flexibility to manage resources and scheduling and mix asynchronous and synchronous processing within the bounds of a non-blocking, asynchronous, push-based stream. +The Reactive Streams contract allows implementations the flexibility to manage resources and scheduling and mix asynchronous and synchronous processing within the bounds of a non-blocking, asynchronous, dynamic push-pull stream. In order to allow fully asynchronous implementations of all participating API elements—`Publisher`/`Subscription`/`Subscriber`/`Processor`—all methods defined by these interfaces return `void`. @@ -235,4 +281,19 @@ Subscribers signaling a demand for one element after the reception of an element ## Legal -This project is a collaboration between engineers from Kaazing, Netflix, Pivotal, RedHat, Twitter, Typesafe and many others. The code is offered to the Public Domain in order to allow free use by interested parties who want to create compatible implementations. For details see `COPYING`. +This project is a collaboration between engineers from Kaazing, Lightbend, Netflix, Pivotal, Red Hat, Twitter and many others. The code is offered to the Public Domain in order to allow free use by interested parties who want to create compatible implementations. For details see `COPYING`. + +

+ + CC0 + +
+ To the extent possible under law, + + Reactive Streams Special Interest Group + has waived all copyright and related or neighboring rights to + Reactive Streams JVM. + This work is published from: + United States. +

+ diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a6431740..75eee4b9 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,3 +1,178 @@ # Release notes for Reactive Streams -Changes will be listed below after version 1.0.0 is released. \ No newline at end of file +--- + +# Version 1.0.1 released on 2017-08-09 + +## Announcement: + +After more than two years since 1.0.0, we are proud to announce the immediate availability of `Reactive Streams version 1.0.1`. + +Since 1.0.0 was released `Reactive Streams` has managed to achieve most, if not all, it set out to achieve. There are now numerous implementations, and it is scheduled to be included in [JDK9](http://download.java.net/java/jdk9/docs/api/java/util/concurrent/Flow.html). + +Also, most importantly, there are no semantical incompatibilities included in this release. + +When JDK9 ships, `Reactive Streams` will publish a compatibility/conversion library to seamlessly convert between the `java.util.concurrent.Flow` and the `org.reactivestreams` namespaces. + +## Highlights: + +- Specification + + A new [Glossary](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.1/README.md#glossary) section + + Description of the intent behind every single rule + + No breaking semantical changes + + Multiple rule [clarifications](#specification-clarifications) +- Interfaces + + No changes + + Improved JavaDoc +- Technology Compatibility Kit (TCK) + + Improved coverage + + Improved JavaDoc + + Multiple test [alterations](#tck-alterations) + +## Specification clarifications + +## Publisher Rule 1 + +**1.0.0:** The total number of onNext signals sent by a Publisher to a Subscriber MUST be less than or equal to the total number of elements requested by that Subscriber´s Subscription at all times. + +**1.0.1:** The total number of onNext´s signalled by a Publisher to a Subscriber MUST be less than or equal to the total number of elements requested by that Subscriber´s Subscription at all times. + +**Comment: Minor spelling update.** + +## Publisher Rule 2 + +**1.0.0:** A Publisher MAY signal less onNext than requested and terminate the Subscription by calling onComplete or onError. + +**1.0.1:** A Publisher MAY signal fewer onNext than requested and terminate the Subscription by calling onComplete or onError. + +**Comment: Minor spelling update.** + +## Publisher Rule 3 + +**1.0.0:** onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled sequentially (no concurrent notifications). + +**1.0.1:** onSubscribe, onNext, onError and onComplete signaled to a Subscriber MUST be signaled in a thread-safe manner—and if performed by multiple threads—use external synchronization. + +**Comment: Reworded the part about sequential signal and its implications, for clarity.** + +## Subscriber Rule 6 + +**1.0.0:** A Subscriber MUST call Subscription.cancel() if it is no longer valid to the Publisher without the Publisher having signaled onError or onComplete. + +**1.0.1:** A Subscriber MUST call Subscription.cancel() if the Subscription is no longer needed. + +**Comment: Rule could be reworded since it now has an intent section describing desired effect.** + +## Subscriber Rule 11 + +**1.0.0:** A Subscriber MUST make sure that all calls on its onXXX methods happen-before [1] the processing of the respective signals. I.e. the Subscriber must take care of properly publishing the signal to its processing logic. + +**1.0.1:** A Subscriber MUST make sure that all calls on its signal methods happen-before the processing of the respective signals. I.e. the Subscriber must take care of properly publishing the signal to its processing logic. + +**Comment: Rule slightly reworded to use the glossary for `signal` instead of the more *ad-hoc* name "onXXX methods". Footnote was reworked into the Intent-section of the rule.** + +## Subscription Rule 1 + +**1.0.0:** Subscription.request and Subscription.cancel MUST only be called inside of its Subscriber context. A Subscription represents the unique relationship between a Subscriber and a Publisher [see 2.12]. + +**1.0.1:** Subscription.request and Subscription.cancel MUST only be called inside of its Subscriber context. + +**Comment: Second part of rule moved into the Intent-section of the rule.** + +## Subscription Rule 3 + +**1.0.0:** Subscription.request MUST place an upper bound on possible synchronous recursion between Publisher and Subscriber[1]. + +**1.0.1:** Subscription.request MUST place an upper bound on possible synchronous recursion between Publisher and Subscriber. + +**Comment: Footnote reworked into the Intent-section of the rule.** + +## Subscription Rule 4 + +**1.0.0:** Subscription.request SHOULD respect the responsivity of its caller by returning in a timely manner[2]. + +**1.0.1:** Subscription.request SHOULD respect the responsivity of its caller by returning in a timely manner. + +**Comment: Footnote reworked into the Intent-section of the rule.** + +## Subscription Rule 5 + +**1.0.0:** Subscription.cancel MUST respect the responsivity of its caller by returning in a timely manner[2], MUST be idempotent and MUST be thread-safe. + +**1.0.1:** Subscription.cancel MUST respect the responsivity of its caller by returning in a timely manner, MUST be idempotent and MUST be thread-safe. + +**Comment: Footnote reworked into the Intent-section of the rule.** + +## Subscription Rule 9 + +**1.0.0:** While the Subscription is not cancelled, Subscription.request(long n) MUST signal onError with a java.lang.IllegalArgumentException if the argument is <= 0. The cause message MUST include a reference to this rule and/or quote the full rule. + +**1.0.1:** While the Subscription is not cancelled, Subscription.request(long n) MUST signal onError with a java.lang.IllegalArgumentException if the argument is <= 0. The cause message SHOULD explain that non-positive request signals are illegal. + +**Comment: The MUST requirement to include a reference to the rule in the exception message has been dropped, in favor of that the exception message SHOULD explain that non-positive requests are illegal.** + +## Subscription Rule 13 + +**1.0.0:** While the Subscription is not cancelled, Subscription.cancel() MUST request the Publisher to eventually drop any references to the corresponding subscriber. Re-subscribing with the same Subscriber object is discouraged [see 2.12], but this specification does not mandate that it is disallowed since that would mean having to store previously cancelled subscriptions indefinitely. + +**1.0.1:** While the Subscription is not cancelled, Subscription.cancel() MUST request the Publisher to eventually drop any references to the corresponding subscriber. + +**Comment: Second part of rule reworked into the Intent-section of the rule.** + +## Subscription Rule 15 + +**1.0.0:** Calling Subscription.cancel MUST return normally. The only legal way to signal failure to a Subscriber is via the onError method. + +**1.0.1:** Calling Subscription.cancel MUST return normally. + +**Comment: Replaced second part of rule with a definition for `return normally` in the glossary.** + +## Subscription Rule 16 + +**1.0.0:** Calling Subscription.request MUST return normally. The only legal way to signal failure to a Subscriber is via the onError method. + +**1.0.1:** Calling Subscription.request MUST return normally. + +**Comment: Replaced second part of rule with a definition for `return normally` in the glossary.** + +## Subscription Rule 17 + +**1.0.0:** A Subscription MUST support an unbounded number of calls to request and MUST support a demand (sum requested - sum delivered) up to 2^63-1 (java.lang.Long.MAX_VALUE). A demand equal or greater than 2^63-1 (java.lang.Long.MAX_VALUE) MAY be considered by the Publisher as “effectively unbounded”[3]. + +**1.0.1:** A Subscription MUST support an unbounded number of calls to request and MUST support a demand up to 2^63-1 (java.lang.Long.MAX_VALUE). A demand equal or greater than 2^63-1 (java.lang.Long.MAX_VALUE) MAY be considered by the Publisher as “effectively unbounded”. + +**Comment: Rule simplified by defining `demand` in the glossary, and footnote was reworked into the Intent-section of the rule.** + +--- + +## TCK alterations + +- Fixed potential resource leaks in partially consuming Publisher tests ([#375](https://github.com/reactive-streams/reactive-streams-jvm/issues/375)) +- Fixed potential resource leaks in partially emitting Subscriber tests ([#372](https://github.com/reactive-streams/reactive-streams-jvm/issues/372), [#373](https://github.com/reactive-streams/reactive-streams-jvm/issues/373)) +- Renamed `untested_spec305_cancelMustNotSynchronouslyPerformHeavyCompuatation` to `untested_spec305_cancelMustNotSynchronouslyPerformHeavyComputation` ([#306](https://github.com/reactive-streams/reactive-streams-jvm/issues/306)) +- Allow configuring separate timeout for "no events during N time", allowing for more aggressive timeouts in the rest of the test suite if required ([#314](https://github.com/reactive-streams/reactive-streams-jvm/issues/314)) +- New test verifying Rule 2.10, in which subscriber must be prepared to receive onError signal without having signaled request before ([#374](https://github.com/reactive-streams/reactive-streams-jvm/issues/374)) +- The verification of Rule 3.9 has been split up into 2 different tests, one to verify that an IllegalArgumentException is sent, and the other an optional check to verify that the exception message informs that non-positive request signals are illegal. +--- + +## Contributors + + Roland Kuhn [(@rkuhn)](https://github.com/rkuhn) + + Ben Christensen [(@benjchristensen)](https://github.com/benjchristensen) + + Viktor Klang [(@viktorklang)](https://github.com/viktorklang) + + Stephane Maldini [(@smaldini)](https://github.com/smaldini) + + Stanislav Savulchik [(@savulchik)](https://github.com/savulchik) + + Konrad Malawski [(@ktoso)](https://github.com/ktoso) + + Slim Ouertani [(@ouertani)](https://github.com/ouertani) + + Martynas Mickevičius [(@2m)](https://github.com/2m) + + Luke Daley [(@ldaley)](https://github.com/ldaley) + + Colin Godsey [(@colinrgodsey)](https://github.com/colinrgodsey) + + David Moten [(@davidmoten)](https://github.com/davidmoten) + + (new) Brian Topping [(@briantopping)](https://github.com/briantopping) + + (new) Rossen Stoyanchev [(@rstoyanchev)](https://github.com/rstoyanchev) + + (new) Björn Hamels [(@BjornHamels)](https://github.com/BjornHamels) + + (new) Jake Wharton [(@JakeWharton)](https://github.com/JakeWharton) + + (new) Anthony Vanelverdinghe[(@anthonyvdotbe)](https://github.com/anthonyvdotbe) + + (new) Kazuhiro Sera [(@seratch)](https://github.com/seratch) + + (new) Dávid Karnok [(@akarnokd)](https://github.com/akarnokd) + + (new) Evgeniy Getman [(@egetman)](https://github.com/egetman) + + (new) Ángel Sanz [(@angelsanz)](https://github.com/angelsanz) diff --git a/api/src/main/java/org/reactivestreams/Processor.java b/api/src/main/java/org/reactivestreams/Processor.java index b22e25f7..8e4ca3dd 100644 --- a/api/src/main/java/org/reactivestreams/Processor.java +++ b/api/src/main/java/org/reactivestreams/Processor.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams; /** diff --git a/api/src/main/java/org/reactivestreams/Publisher.java b/api/src/main/java/org/reactivestreams/Publisher.java index b97a9fc1..caf6343d 100644 --- a/api/src/main/java/org/reactivestreams/Publisher.java +++ b/api/src/main/java/org/reactivestreams/Publisher.java @@ -1,10 +1,21 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams; /** * A {@link Publisher} is a provider of a potentially unbounded number of sequenced elements, publishing them according to * the demand received from its {@link Subscriber}(s). *

- * A {@link Publisher} can serve multiple {@link Subscriber}s subscribed {@link #subscribe(Subscriber)}dynamically + * A {@link Publisher} can serve multiple {@link Subscriber}s subscribed {@link #subscribe(Subscriber)} dynamically * at various points in time. * * @param the type of element signaled. diff --git a/api/src/main/java/org/reactivestreams/Subscriber.java b/api/src/main/java/org/reactivestreams/Subscriber.java index 6165352e..b9299ddc 100644 --- a/api/src/main/java/org/reactivestreams/Subscriber.java +++ b/api/src/main/java/org/reactivestreams/Subscriber.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams; /** diff --git a/api/src/main/java/org/reactivestreams/Subscription.java b/api/src/main/java/org/reactivestreams/Subscription.java index bab4236d..c18d0fc9 100644 --- a/api/src/main/java/org/reactivestreams/Subscription.java +++ b/api/src/main/java/org/reactivestreams/Subscription.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams; /** @@ -27,7 +38,7 @@ public interface Subscription { /** * Request the {@link Publisher} to stop sending data and clean up resources. *

- * Data may still be sent to meet previously signalled demand after calling cancel as this request is asynchronous. + * Data may still be sent to meet previously signalled demand after calling cancel. */ public void cancel(); } diff --git a/build.gradle b/build.gradle index eecb748a..7f4708a1 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ subprojects { apply plugin: "osgi" group = "org.reactivestreams" - version = "1.0.0.RC1" + version = "1.0.1" sourceCompatibility = 1.6 targetCompatibility = 1.6 @@ -28,7 +28,7 @@ subprojects { tasks.withType(Test) { testLogging { exceptionFormat "full" - events "failed", "started" + events "failed", "started", "standard_out", "standard_error" } } @@ -36,15 +36,21 @@ subprojects { mavenCentral() } + jar { manifest { - instruction "Bundle-Vendor", "Reactive Streams SIG" - instruction "Bundle-Description", "Reactive Streams API" - instruction "Bundle-DocURL", "http://reactive-streams.org" + instructionReplace "Bundle-Vendor", "Reactive Streams SIG" + instructionReplace "Bundle-Description", "Reactive Streams API" + instructionReplace "Bundle-DocURL", "http://reactive-streams.org" + instructionReplace "Bundle-Version", "1.0.1" } } - if (name in ["reactive-streams", "reactive-streams-tck"]) { + if (name in ["reactive-streams", + "reactive-streams-tck", + "reactive-streams-tck-flow", + "reactive-streams-examples", + "reactive-streams-flow-bridge"]) { apply plugin: "maven" apply plugin: "signing" diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/AsyncIterablePublisher.java b/examples/src/main/java/org/reactivestreams/example/unicast/AsyncIterablePublisher.java index 985144a8..2dee33da 100644 --- a/examples/src/main/java/org/reactivestreams/example/unicast/AsyncIterablePublisher.java +++ b/examples/src/main/java/org/reactivestreams/example/unicast/AsyncIterablePublisher.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Publisher; @@ -66,6 +77,7 @@ final class SubscriptionImpl implements Subscription, Runnable { private Iterator iterator; // This is our cursor into the data stream, which we will send to the `Subscriber` SubscriptionImpl(final Subscriber subscriber) { + // As per rule 1.09, we need to throw a `java.lang.NullPointerException` if the `Subscriber` is `null` if (subscriber == null) throw null; this.subscriber = subscriber; } @@ -105,7 +117,11 @@ private void doSubscribe() { if (iterator == null) iterator = Collections.emptyList().iterator(); // So we can assume that `iterator` is never null } catch(final Throwable t) { - terminateDueTo(t); // Here we send onError, obeying rule 1.12 + subscriber.onSubscribe(new Subscription() { // We need to make sure we signal onSubscribe before onError, obeying rule 1.9 + @Override public void cancel() {} + @Override public void request(long n) {} + }); + terminateDueTo(t); // Here we send onError, obeying rule 1.09 } if (!cancelled) { @@ -176,7 +192,7 @@ private void terminateDueTo(final Throwable t) { cancelled = true; // When we signal onError, the subscription must be considered as cancelled, as per rule 1.6 try { subscriber.onError(t); // Then we signal the error downstream, to the `Subscriber` - } catch(final Throwable t2) { // If `onError` throws an exception, this is a spec violation according to rule 1.13, and all we can do is to log it. + } catch(final Throwable t2) { // If `onError` throws an exception, this is a spec violation according to rule 1.9, and all we can do is to log it. (new IllegalStateException(subscriber + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); } } diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/AsyncSubscriber.java b/examples/src/main/java/org/reactivestreams/example/unicast/AsyncSubscriber.java index d5660067..00ea1685 100644 --- a/examples/src/main/java/org/reactivestreams/example/unicast/AsyncSubscriber.java +++ b/examples/src/main/java/org/reactivestreams/example/unicast/AsyncSubscriber.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Subscriber; @@ -50,7 +61,7 @@ protected AsyncSubscriber(Executor executor) { // herefor we also need to cancel our `Subscription`. private final void done() { //On this line we could add a guard against `!done`, but since rule 3.7 says that `Subscription.cancel()` is idempotent, we don't need to. - done = true; // If we `foreach` throws an exception, let's consider ourselves done (not accepting more elements) + done = true; // If `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) if (subscription != null) { // If we are bailing out before we got a `Subscription` there's little need for cancelling it. try { subscription.cancel(); // Cancel the subscription @@ -94,15 +105,16 @@ private final void handleOnSubscribe(final Subscription s) { s.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time } catch(final Throwable t) { // Subscription.request is not allowed to throw according to rule 3.16 - (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from cancel.", t)).printStackTrace(System.err); + (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); } } } private final void handleOnNext(final T element) { if (!done) { // If we aren't already done - if(subscription == null) { // Check for spec violation of 2.1 - (new IllegalStateException("Someone violated the Reactive Streams rule 2.1 by signalling OnNext before `Subscription.request`. (no Subscription)")).printStackTrace(System.err); + if(subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + // Check for spec violation of 2.1 and 1.09 + (new IllegalStateException("Someone violated the Reactive Streams rule 1.09 and 2.1 by signalling OnNext before `Subscription.request`. (no Subscription)")).printStackTrace(System.err); } else { try { if (whenNext(element)) { @@ -110,13 +122,13 @@ private final void handleOnNext(final T element) { subscription.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time } catch(final Throwable t) { // Subscription.request is not allowed to throw according to rule 3.16 - (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.16 by throwing an exception from cancel.", t)).printStackTrace(System.err); + (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); } } else { done(); // This is legal according to rule 2.6 } } catch(final Throwable t) { - done(); + done(); try { onError(t); } catch(final Throwable t2) { @@ -130,28 +142,47 @@ private final void handleOnNext(final T element) { // Here it is important that we do not violate 2.2 and 2.3 by calling methods on the `Subscription` or `Publisher` private void handleOnComplete() { - done = true; // Obey rule 2.4 - whenComplete(); + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + // Publisher is not allowed to signal onComplete before onSubscribe according to rule 1.09 + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); + } else { + done = true; // Obey rule 2.4 + whenComplete(); + } } // Here it is important that we do not violate 2.2 and 2.3 by calling methods on the `Subscription` or `Publisher` private void handleOnError(final Throwable error) { - done = true; // Obey rule 2.4 - whenError(error); + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + // Publisher is not allowed to signal onError before onSubscribe according to rule 1.09 + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); + } else { + done = true; // Obey rule 2.4 + whenError(error); + } } // We implement the OnX methods on `Subscriber` to send Signals that we will process asycnhronously, but only one at a time @Override public final void onSubscribe(final Subscription s) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` + if (s == null) throw null; + signal(new OnSubscribe(s)); } @Override public final void onNext(final T element) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` + if (element == null) throw null; + signal(new OnNext(element)); } @Override public final void onError(final Throwable t) { - signal(new OnError(t)); + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` + if (t == null) throw null; + + signal(new OnError(t)); } @Override public final void onComplete() { @@ -172,7 +203,6 @@ private void handleOnError(final Throwable error) { try { final Signal s = inboundSignals.poll(); // We take a signal off the queue if (!done) { // If we're done, we shouldn't process any more signals, obeying rule 2.8 - // Below we simply unpack the `Signal`s and invoke the corresponding methods if (s instanceof OnNext) handleOnNext(((OnNext)s).next); @@ -180,7 +210,7 @@ else if (s instanceof OnSubscribe) handleOnSubscribe(((OnSubscribe)s).subscription); else if (s instanceof OnError) // We are always able to handle OnError, obeying rule 2.10 handleOnError(((OnError)s).error); - else if (s == OnComplete.Instance) // We are always able to handle OnError, obeying rule 2.9 + else if (s == OnComplete.Instance) // We are always able to handle OnComplete, obeying rule 2.9 handleOnComplete(); } } finally { @@ -216,4 +246,4 @@ private final void tryScheduleToExecute() { } } } -} \ No newline at end of file +} diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/InfiniteIncrementNumberPublisher.java b/examples/src/main/java/org/reactivestreams/example/unicast/InfiniteIncrementNumberPublisher.java index 0e82c0fd..6cb46015 100644 --- a/examples/src/main/java/org/reactivestreams/example/unicast/InfiniteIncrementNumberPublisher.java +++ b/examples/src/main/java/org/reactivestreams/example/unicast/InfiniteIncrementNumberPublisher.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import java.util.Iterator; diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/NumberIterablePublisher.java b/examples/src/main/java/org/reactivestreams/example/unicast/NumberIterablePublisher.java index 20e19fec..756bcfab 100644 --- a/examples/src/main/java/org/reactivestreams/example/unicast/NumberIterablePublisher.java +++ b/examples/src/main/java/org/reactivestreams/example/unicast/NumberIterablePublisher.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import java.util.Collections; diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/RangePublisher.java b/examples/src/main/java/org/reactivestreams/example/unicast/RangePublisher.java new file mode 100644 index 00000000..67c3103d --- /dev/null +++ b/examples/src/main/java/org/reactivestreams/example/unicast/RangePublisher.java @@ -0,0 +1,242 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.example.unicast; + +import org.reactivestreams.*; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * A synchronous implementation of the {@link Publisher} that can + * be subscribed to multiple times and each individual subscription + * will receive range of monotonically increasing integer values on demand. + */ +public final class RangePublisher implements Publisher { + + /** The starting value of the range. */ + final int start; + + /** The number of items to emit. */ + final int count; + + /** + * Constructs a RangePublisher instance with the given start and count values + * that yields a sequence of [start, start + count). + * @param start the starting value of the range + * @param count the number of items to emit + */ + public RangePublisher(int start, int count) { + this.start = start; + this.count = count; + } + + @Override + public void subscribe(Subscriber subscriber) { + // As per rule 1.11, we have decided to support multiple subscribers + // in a unicast configuration for this `Publisher` implementation. + + // As per rule 1.09, we need to throw a `java.lang.NullPointerException` + // if the `Subscriber` is `null` + if (subscriber == null) throw null; + + // As per 2.13, this method must return normally (i.e. not throw). + try { + subscriber.onSubscribe(new RangeSubscription(subscriber, start, start + count)); + } catch (Throwable ex) { + new IllegalStateException(subscriber + " violated the Reactive Streams rule 2.13 " + + "by throwing an exception from onSubscribe.", ex) + // When onSubscribe fails this way, we don't know what state the + // subscriber is thus calling onError may cause more crashes. + .printStackTrace(); + } + } + + /** + * A Subscription implementation that holds the current downstream + * requested amount and responds to the downstream's request() and + * cancel() calls. + */ + static final class RangeSubscription + // We are using this `AtomicLong` to make sure that this `Subscription` + // doesn't run concurrently with itself, which would violate rule 1.3 + // among others (no concurrent notifications). + // The atomic transition from 0L to N > 0L will ensure this. + extends AtomicLong implements Subscription { + + private static final long serialVersionUID = -9000845542177067735L; + + /** The Subscriber we are emitting integer values to. */ + final Subscriber downstream; + + /** The end index (exclusive). */ + final int end; + + /** + * The current index and within the [start, start + count) range that + * will be emitted as downstream.onNext(). + */ + int index; + + /** + * Indicates the emission should stop. + */ + volatile boolean cancelled; + + /** + * Holds onto the IllegalArgumentException (containing the offending stacktrace) + * indicating there was a non-positive request() call from the downstream. + */ + volatile Throwable invalidRequest; + + /** + * Constructs a stateful RangeSubscription that emits signals to the given + * downstream from an integer range of [start, end). + * @param downstream the Subscriber receiving the integer values and the completion signal. + * @param start the first integer value emitted, start of the range + * @param end the end of the range, exclusive + */ + RangeSubscription(Subscriber downstream, int start, int end) { + this.downstream = downstream; + this.index = start; + this.end = end; + } + + // This method will register inbound demand from our `Subscriber` and + // validate it against rule 3.9 and rule 3.17 + @Override + public void request(long n) { + // Non-positive requests should be honored with IllegalArgumentException + if (n <= 0L) { + invalidRequest = new IllegalArgumentException("§3.9: non-positive requests are not allowed!"); + n = 1; + } + // Downstream requests are cumulative and may come from any thread + for (;;) { + long requested = get(); + long update = requested + n; + // As governed by rule 3.17, when demand overflows `Long.MAX_VALUE` + // we treat the signalled demand as "effectively unbounded" + if (update < 0L) { + update = Long.MAX_VALUE; + } + // atomically update the current requested amount + if (compareAndSet(requested, update)) { + // if there was no prior request amount, we start the emission loop + if (requested == 0L) { + emit(update); + } + break; + } + } + } + + // This handles cancellation requests, and is idempotent, thread-safe and not + // synchronously performing heavy computations as specified in rule 3.5 + @Override + public void cancel() { + // Indicate to the emission loop it should stop. + cancelled = true; + } + + void emit(long currentRequested) { + // Load fields to avoid re-reading them from memory due to volatile accesses in the loop. + Subscriber downstream = this.downstream; + int index = this.index; + int end = this.end; + int emitted = 0; + + try { + for (; ; ) { + // Check if there was an invalid request and then report its exception + // as mandated by rule 3.9. The stacktrace in it should + // help locate the faulty logic in the Subscriber. + Throwable invalidRequest = this.invalidRequest; + if (invalidRequest != null) { + // When we signal onError, the subscription must be considered as cancelled, as per rule 1.6 + cancelled = true; + + downstream.onError(invalidRequest); + return; + } + + // Loop while the index hasn't reached the end and we haven't + // emitted all that's been requested + while (index != end && emitted != currentRequested) { + // to make sure that we follow rule 1.8, 3.6 and 3.7 + // We stop if cancellation was requested. + if (cancelled) { + return; + } + + downstream.onNext(index); + + // Increment the index for the next possible emission. + index++; + // Increment the emitted count to prevent overflowing the downstream. + emitted++; + } + + // If the index reached the end, we complete the downstream. + if (index == end) { + // to make sure that we follow rule 1.8, 3.6 and 3.7 + // Unless cancellation was requested by the last onNext. + if (!cancelled) { + // We need to consider this `Subscription` as cancelled as per rule 1.6 + // Note, however, that this state is not observable from the outside + // world and since we leave the loop with requested > 0L, any + // further request() will never trigger the loop. + cancelled = true; + + downstream.onComplete(); + } + return; + } + + // Did the requested amount change while we were looping? + long freshRequested = get(); + if (freshRequested == currentRequested) { + // Save where the loop has left off: the next value to be emitted + this.index = index; + // Atomically subtract the previously requested (also emitted) amount + currentRequested = addAndGet(-currentRequested); + // If there was no new request in between get() and addAndGet(), we simply quit + // The next 0 to N transition in request() will trigger the next emission loop. + if (currentRequested == 0L) { + break; + } + // Looks like there were more async requests, reset the emitted count and continue. + emitted = 0; + } else { + // Yes, avoid the atomic subtraction and resume. + // emitted != currentRequest in this case and index + // still points to the next value to be emitted + currentRequested = freshRequested; + } + } + } catch (Throwable ex) { + // We can only get here if `onNext`, `onError` or `onComplete` threw, and they + // are not allowed to according to 2.13, so we can only cancel and log here. + // If `onError` throws an exception, this is a spec violation according to rule 1.9, + // and all we can do is to log it. + + // Make sure that we are cancelled, since we cannot do anything else + // since the `Subscriber` is faulty. + cancelled = true; + + // We can't report the failure to onError as the Subscriber is unreliable. + (new IllegalStateException(downstream + " violated the Reactive Streams rule 2.13 by " + + "throwing an exception from onNext, onError or onComplete.", ex)) + .printStackTrace(); + } + } + } +} diff --git a/examples/src/main/java/org/reactivestreams/example/unicast/SyncSubscriber.java b/examples/src/main/java/org/reactivestreams/example/unicast/SyncSubscriber.java index 6f2ef338..e1777ff0 100644 --- a/examples/src/main/java/org/reactivestreams/example/unicast/SyncSubscriber.java +++ b/examples/src/main/java/org/reactivestreams/example/unicast/SyncSubscriber.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Subscriber; @@ -15,6 +26,9 @@ public abstract class SyncSubscriber implements Subscriber { private boolean done = false; @Override public void onSubscribe(final Subscription s) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` + if (s == null) throw null; + if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully try { s.cancel(); // Cancel the additional subscription @@ -32,31 +46,38 @@ public abstract class SyncSubscriber implements Subscriber { s.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time } catch(final Throwable t) { // Subscription.request is not allowed to throw according to rule 3.16 - (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from cancel.", t)).printStackTrace(System.err); + (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); } } } @Override public void onNext(final T element) { - if (!done) { // If we aren't already done - try { - if (foreach(element)) { - try { - subscription.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time - } catch(final Throwable t) { - // Subscription.request is not allowed to throw according to rule 3.16 - (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.16 by throwing an exception from cancel.", t)).printStackTrace(System.err); + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` + if (element == null) throw null; + + if (!done) { // If we aren't already done + try { + if (whenNext(element)) { + try { + subscription.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time + } catch (final Throwable t) { + // Subscription.request is not allowed to throw according to rule 3.16 + (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); + } + } else { + done(); } - } else { + } catch (final Throwable t) { done(); - } - } catch(final Throwable t) { - done(); - try { - onError(t); - } catch(final Throwable t2) { - //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 - (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); + try { + onError(t); + } catch (final Throwable t2) { + //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 + (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); + } } } } @@ -66,7 +87,7 @@ public abstract class SyncSubscriber implements Subscriber { // herefor we also need to cancel our `Subscription`. private void done() { //On this line we could add a guard against `!done`, but since rule 3.7 says that `Subscription.cancel()` is idempotent, we don't need to. - done = true; // If we `foreach` throws an exception, let's consider ourselves done (not accepting more elements) + done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) try { subscription.cancel(); // Cancel the subscription } catch(final Throwable t) { @@ -77,15 +98,25 @@ private void done() { // This method is left as an exercise to the reader/extension point // Returns whether more elements are desired or not, and if no more elements are desired - protected abstract boolean foreach(final T element); + protected abstract boolean whenNext(final T element); @Override public void onError(final Throwable t) { - // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 - // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` + if (t == null) throw null; + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } } @Override public void onComplete() { - // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 - // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); + } else { + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } } } \ No newline at end of file diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/AsyncSubscriberTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/AsyncSubscriberTest.java index 596ae1da..f11dbaea 100644 --- a/examples/src/test/java/org/reactivestreams/example/unicast/AsyncSubscriberTest.java +++ b/examples/src/test/java/org/reactivestreams/example/unicast/AsyncSubscriberTest.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Publisher; diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/IterablePublisherTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/IterablePublisherTest.java index 15d5ec9a..23991190 100644 --- a/examples/src/test/java/org/reactivestreams/example/unicast/IterablePublisherTest.java +++ b/examples/src/test/java/org/reactivestreams/example/unicast/IterablePublisherTest.java @@ -1,5 +1,17 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; +import java.lang.Override; import java.util.Collections; import java.util.Iterator; import org.reactivestreams.Publisher; @@ -29,8 +41,12 @@ public IterablePublisherTest() { return new NumberIterablePublisher(0, (int)elements, e); } - @Override public Publisher createErrorStatePublisher() { - return null; + @Override public Publisher createFailedPublisher() { + return new AsyncIterablePublisher(new Iterable() { + @Override public Iterator iterator() { + throw new RuntimeException("Error state signal!"); + } + }, e); } @Override public long maxElementsFromPublisher() { diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/RangePublisherTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/RangePublisherTest.java new file mode 100644 index 00000000..964d395e --- /dev/null +++ b/examples/src/test/java/org/reactivestreams/example/unicast/RangePublisherTest.java @@ -0,0 +1,32 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.example.unicast; + +import org.reactivestreams.Publisher; +import org.reactivestreams.example.unicast.RangePublisher; +import org.reactivestreams.tck.*; + +public class RangePublisherTest extends PublisherVerification { + public RangePublisherTest() { + super(new TestEnvironment(50, 50)); + } + + @Override + public Publisher createPublisher(long elements) { + return new RangePublisher(1, (int)elements); + } + + @Override + public Publisher createFailedPublisher() { + return null; + } +} diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberTest.java index c9303a08..ac3e34f5 100644 --- a/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberTest.java +++ b/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberTest.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Subscriber; @@ -24,7 +35,7 @@ public SyncSubscriberTest() { @Override public Subscriber createSubscriber() { return new SyncSubscriber() { private long acc; - @Override protected boolean foreach(final Integer element) { + @Override protected boolean whenNext(final Integer element) { acc += element; return true; } diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberWhiteboxTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberWhiteboxTest.java new file mode 100644 index 00000000..06f7cc9b --- /dev/null +++ b/examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberWhiteboxTest.java @@ -0,0 +1,86 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + +package org.reactivestreams.example.unicast; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.SubscriberBlackboxVerification; +import org.reactivestreams.tck.SubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Test // Must be here for TestNG to find and run this, do not remove +public class SyncSubscriberWhiteboxTest extends SubscriberWhiteboxVerification { + + private ExecutorService e; + @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } + @AfterClass void after() { if (e != null) e.shutdown(); } + + public SyncSubscriberWhiteboxTest() { + super(new TestEnvironment()); + } + + @Override + public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { + return new SyncSubscriber() { + @Override + public void onSubscribe(final Subscription s) { + super.onSubscribe(s); + + probe.registerOnSubscribe(new SubscriberPuppet() { + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + } + + @Override + public void onNext(Integer element) { + super.onNext(element); + probe.registerOnNext(element); + } + + @Override + public void onError(Throwable cause) { + super.onError(cause); + probe.registerOnError(cause); + } + + @Override + public void onComplete() { + super.onComplete(); + probe.registerOnComplete(); + } + + @Override + protected boolean whenNext(Integer element) { + return true; + } + }; + } + + @Override public Integer createElement(int element) { + return element; + } + +} diff --git a/examples/src/test/java/org/reactivestreams/example/unicast/UnboundedIntegerIncrementPublisherTest.java b/examples/src/test/java/org/reactivestreams/example/unicast/UnboundedIntegerIncrementPublisherTest.java index 40548933..476ff703 100644 --- a/examples/src/test/java/org/reactivestreams/example/unicast/UnboundedIntegerIncrementPublisherTest.java +++ b/examples/src/test/java/org/reactivestreams/example/unicast/UnboundedIntegerIncrementPublisherTest.java @@ -1,3 +1,14 @@ +/************************************************************************ +* Licensed under Public Domain (CC0) * +* * +* To the extent possible under law, the person who associated CC0 with * +* this code has waived all copyright and related or neighboring * +* rights to this code. * +* * +* You should have received a copy of the CC0 legalcode along with this * +* work. If not, see .* +************************************************************************/ + package org.reactivestreams.example.unicast; import org.reactivestreams.Publisher; @@ -8,6 +19,7 @@ import org.testng.annotations.AfterClass; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService; +import java.util.Iterator; @Test // Must be here for TestNG to find and run this, do not remove public class UnboundedIntegerIncrementPublisherTest extends PublisherVerification { @@ -24,8 +36,12 @@ public UnboundedIntegerIncrementPublisherTest() { return new InfiniteIncrementNumberPublisher(e); } - @Override public Publisher createErrorStatePublisher() { - return null; + @Override public Publisher createFailedPublisher() { + return new AsyncIterablePublisher(new Iterable() { + @Override public Iterator iterator() { + throw new RuntimeException("Error state signal!"); + } + }, e); } @Override public long maxElementsFromPublisher() { diff --git a/flow-bridge/.gitignore b/flow-bridge/.gitignore new file mode 100644 index 00000000..7447f89a --- /dev/null +++ b/flow-bridge/.gitignore @@ -0,0 +1 @@ +/bin \ No newline at end of file diff --git a/flow-bridge/build.gradle b/flow-bridge/build.gradle new file mode 100644 index 00000000..13debe28 --- /dev/null +++ b/flow-bridge/build.gradle @@ -0,0 +1,13 @@ +description = 'reactive-streams-flow-bridge' + +dependencies { + compile project(':reactive-streams') + + testCompile project(':reactive-streams-tck') + testCompile group: 'org.testng', name: 'testng', version: '5.14.10' +} +test.useTestNG() + +javadoc { + options.links("http://download.java.net/java/jdk9/docs/api") +} \ No newline at end of file diff --git a/flow-bridge/src/main/java/org/reactivestreams/ReactiveStreamsFlowBridge.java b/flow-bridge/src/main/java/org/reactivestreams/ReactiveStreamsFlowBridge.java new file mode 100644 index 00000000..e45fd548 --- /dev/null +++ b/flow-bridge/src/main/java/org/reactivestreams/ReactiveStreamsFlowBridge.java @@ -0,0 +1,381 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams; + +import java.util.concurrent.Flow; + +/** + * Bridge between Reactive Streams API and the Java 9 {@link java.util.concurrent.Flow} API. + */ +public final class ReactiveStreamsFlowBridge { + /** Utility class. */ + private ReactiveStreamsFlowBridge() { + throw new IllegalStateException("No instances!"); + } + + /** + * Converts a Flow Publisher into a Reactive Streams Publisher. + * @param the element type + * @param flowPublisher the source Flow Publisher to convert + * @return the equivalent Reactive Streams Publisher + */ + @SuppressWarnings("unchecked") + public static org.reactivestreams.Publisher toPublisher( + Flow.Publisher flowPublisher) { + if (flowPublisher == null) { + throw new NullPointerException("flowPublisher"); + } + if (flowPublisher instanceof FlowPublisherFromReactive) { + return (org.reactivestreams.Publisher)(((FlowPublisherFromReactive)flowPublisher).reactiveStreams); + } + if (flowPublisher instanceof org.reactivestreams.Publisher) { + return (org.reactivestreams.Publisher)flowPublisher; + } + return new ReactivePublisherFromFlow(flowPublisher); + } + + /** + * Converts a Reactive Streams Publisher into a Flow Publisher. + * @param the element type + * @param reactiveStreamsPublisher the source Reactive Streams Publisher to convert + * @return the equivalent Flow Publisher + */ + @SuppressWarnings("unchecked") + public static Flow.Publisher toFlowPublisher( + org.reactivestreams.Publisher reactiveStreamsPublisher + ) { + if (reactiveStreamsPublisher == null) { + throw new NullPointerException("reactiveStreamsPublisher"); + } + if (reactiveStreamsPublisher instanceof ReactivePublisherFromFlow) { + return (Flow.Publisher)(((ReactivePublisherFromFlow)reactiveStreamsPublisher).flow); + } + if (reactiveStreamsPublisher instanceof Flow.Publisher) { + return (Flow.Publisher)reactiveStreamsPublisher; + } + return new FlowPublisherFromReactive(reactiveStreamsPublisher); + } + + /** + * Converts a Flow Processor into a Reactive Streams Processor. + * @param the input value type + * @param the output value type + * @param flowProcessor the source Flow Processor to convert + * @return the equivalent Reactive Streams Processor + */ + @SuppressWarnings("unchecked") + public static org.reactivestreams.Processor toProcessor( + Flow.Processor flowProcessor + ) { + if (flowProcessor == null) { + throw new NullPointerException("flowProcessor"); + } + if (flowProcessor instanceof FlowToReactiveProcessor) { + return (org.reactivestreams.Processor)(((FlowToReactiveProcessor)flowProcessor).reactiveStreams); + } + if (flowProcessor instanceof org.reactivestreams.Processor) { + return (org.reactivestreams.Processor)flowProcessor; + } + return new ReactiveToFlowProcessor(flowProcessor); + } + + /** + * Converts a Reactive Streams Processor into a Flow Processor. + * @param the input value type + * @param the output value type + * @param reactiveStreamsProcessor the source Reactive Streams Processor to convert + * @return the equivalent Flow Processor + */ + @SuppressWarnings("unchecked") + public static Flow.Processor toFlowProcessor( + org.reactivestreams.Processor reactiveStreamsProcessor + ) { + if (reactiveStreamsProcessor == null) { + throw new NullPointerException("reactiveStreamsProcessor"); + } + if (reactiveStreamsProcessor instanceof ReactiveToFlowProcessor) { + return (Flow.Processor)(((ReactiveToFlowProcessor)reactiveStreamsProcessor).flow); + } + if (reactiveStreamsProcessor instanceof Flow.Processor) { + return (Flow.Processor)reactiveStreamsProcessor; + } + return new FlowToReactiveProcessor(reactiveStreamsProcessor); + } + + /** + * Converts a Reactive Streams Subscriber into a Flow Subscriber. + * @param the input and output value type + * @param reactiveStreamsSubscriber the Reactive Streams Subscriber instance to convert + * @return the equivalent Flow Subscriber + */ + @SuppressWarnings("unchecked") + public static Flow.Subscriber toFlowSubscriber(org.reactivestreams.Subscriber reactiveStreamsSubscriber) { + if (reactiveStreamsSubscriber == null) { + throw new NullPointerException("reactiveStreamsSubscriber"); + } + if (reactiveStreamsSubscriber instanceof ReactiveToFlowSubscriber) { + return (Flow.Subscriber)((ReactiveToFlowSubscriber)reactiveStreamsSubscriber).flow; + } + if (reactiveStreamsSubscriber instanceof Flow.Subscriber) { + return (Flow.Subscriber)reactiveStreamsSubscriber; + } + return new FlowToReactiveSubscriber(reactiveStreamsSubscriber); + } + + /** + * Converts a Flow Subscriber into a Reactive Streams Subscriber. + * @param the input and output value type + * @param flowSubscriber the Flow Subscriber instance to convert + * @return the equivalent Reactive Streams Subscriber + */ + @SuppressWarnings("unchecked") + public static org.reactivestreams.Subscriber toSubscriber(Flow.Subscriber flowSubscriber) { + if (flowSubscriber == null) { + throw new NullPointerException("flowSubscriber"); + } + if (flowSubscriber instanceof FlowToReactiveSubscriber) { + return (org.reactivestreams.Subscriber)((FlowToReactiveSubscriber)flowSubscriber).reactiveStreams; + } + if (flowSubscriber instanceof org.reactivestreams.Subscriber) { + return (org.reactivestreams.Subscriber)flowSubscriber; + } + return new ReactiveToFlowSubscriber(flowSubscriber); + } + + /** + * Wraps a Reactive Streams Subscription and converts the calls to a Flow Subscription. + */ + static final class FlowToReactiveSubscription implements Flow.Subscription { + final org.reactivestreams.Subscription reactiveStreams; + + public FlowToReactiveSubscription(org.reactivestreams.Subscription reactive) { + this.reactiveStreams = reactive; + } + + @Override + public void request(long n) { + reactiveStreams.request(n); + } + + @Override + public void cancel() { + reactiveStreams.cancel(); + } + + } + + /** + * Wraps a Flow Subscription and converts the calls to a Reactive Streams Subscription. + */ + static final class ReactiveToFlowSubscription implements org.reactivestreams.Subscription { + final Flow.Subscription flow; + + public ReactiveToFlowSubscription(Flow.Subscription flow) { + this.flow = flow; + } + + @Override + public void request(long n) { + flow.request(n); + } + + @Override + public void cancel() { + flow.cancel(); + } + + + } + + /** + * Wraps a Reactive Streams Subscriber and forwards methods of the Flow Subscriber to it. + * @param the element type + */ + static final class FlowToReactiveSubscriber + implements Flow.Subscriber { + final org.reactivestreams.Subscriber reactiveStreams; + + public FlowToReactiveSubscriber(org.reactivestreams.Subscriber reactive) { + this.reactiveStreams = reactive; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + reactiveStreams.onSubscribe((subscription == null) ? null : new ReactiveToFlowSubscription(subscription)); + } + + @Override + public void onNext(T item) { + reactiveStreams.onNext(item); + } + + @Override + public void onError(Throwable throwable) { + reactiveStreams.onError(throwable); + } + + @Override + public void onComplete() { + reactiveStreams.onComplete(); + } + + } + + /** + * Wraps a Reactive Streams Subscriber and forwards methods of the Flow Subscriber to it. + * @param the element type + */ + static final class ReactiveToFlowSubscriber + implements org.reactivestreams.Subscriber { + final Flow.Subscriber flow; + + public ReactiveToFlowSubscriber(Flow.Subscriber flow) { + this.flow = flow; + } + + @Override + public void onSubscribe(org.reactivestreams.Subscription subscription) { + flow.onSubscribe((subscription == null) ? null : new FlowToReactiveSubscription(subscription)); + } + + @Override + public void onNext(T item) { + flow.onNext(item); + } + + @Override + public void onError(Throwable throwable) { + flow.onError(throwable); + } + + @Override + public void onComplete() { + flow.onComplete(); + } + + } + + /** + * Wraps a Flow Processor and forwards methods of the Reactive Streams Processor to it. + * @param the input type + * @param the output type + */ + static final class ReactiveToFlowProcessor + implements org.reactivestreams.Processor { + final Flow.Processor flow; + + public ReactiveToFlowProcessor(Flow.Processor flow) { + this.flow = flow; + } + + @Override + public void onSubscribe(org.reactivestreams.Subscription subscription) { + flow.onSubscribe((subscription == null) ? null : new FlowToReactiveSubscription(subscription)); + } + + @Override + public void onNext(T t) { + flow.onNext(t); + } + + @Override + public void onError(Throwable t) { + flow.onError(t); + } + + @Override + public void onComplete() { + flow.onComplete(); + } + + @Override + public void subscribe(org.reactivestreams.Subscriber s) { + flow.subscribe((s == null) ? null : new FlowToReactiveSubscriber(s)); + } + } + + /** + * Wraps a Reactive Streams Processor and forwards methods of the Flow Processor to it. + * @param the input type + * @param the output type + */ + static final class FlowToReactiveProcessor + implements Flow.Processor { + final org.reactivestreams.Processor reactiveStreams; + + public FlowToReactiveProcessor(org.reactivestreams.Processor reactive) { + this.reactiveStreams = reactive; + } + + @Override + public void onSubscribe(Flow.Subscription subscription) { + reactiveStreams.onSubscribe((subscription == null) ? null : new ReactiveToFlowSubscription(subscription)); + } + + @Override + public void onNext(T t) { + reactiveStreams.onNext(t); + } + + @Override + public void onError(Throwable t) { + reactiveStreams.onError(t); + } + + @Override + public void onComplete() { + reactiveStreams.onComplete(); + } + + @Override + public void subscribe(Flow.Subscriber s) { + reactiveStreams.subscribe((s == null) ? null : new ReactiveToFlowSubscriber(s)); + } + } + + /** + * Reactive Streams Publisher that wraps a Flow Publisher. + * @param the element type + */ + static final class ReactivePublisherFromFlow implements org.reactivestreams.Publisher { + + final Flow.Publisher flow; + + public ReactivePublisherFromFlow(Flow.Publisher flowPublisher) { + this.flow = flowPublisher; + } + + @Override + public void subscribe(org.reactivestreams.Subscriber reactive) { + flow.subscribe((reactive == null) ? null : new FlowToReactiveSubscriber(reactive)); + } + } + + /** + * Flow Publisher that wraps a Reactive Streams Publisher. + * @param the element type + */ + static final class FlowPublisherFromReactive implements Flow.Publisher { + + final org.reactivestreams.Publisher reactiveStreams; + + public FlowPublisherFromReactive(org.reactivestreams.Publisher reactivePublisher) { + this.reactiveStreams = reactivePublisher; + } + + @Override + public void subscribe(Flow.Subscriber flow) { + reactiveStreams.subscribe((flow == null) ? null : new ReactiveToFlowSubscriber(flow)); + } + } + +} diff --git a/flow-bridge/src/test/java/org/reactivestreams/MulticastPublisher.java b/flow-bridge/src/test/java/org/reactivestreams/MulticastPublisher.java new file mode 100644 index 00000000..31f00543 --- /dev/null +++ b/flow-bridge/src/test/java/org/reactivestreams/MulticastPublisher.java @@ -0,0 +1,358 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.Objects; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.*; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.atomic.*; + +final class MulticastPublisher implements Publisher, AutoCloseable { + + final Executor executor; + final int bufferSize; + + final AtomicBoolean done = new AtomicBoolean(); + Throwable error; + + static final InnerSubscription[] EMPTY = new InnerSubscription[0]; + static final InnerSubscription[] TERMINATED = new InnerSubscription[0]; + + + final AtomicReference[]> subscribers = new AtomicReference[]>(); + + public MulticastPublisher() { + this(ForkJoinPool.commonPool(), Flow.defaultBufferSize()); + } + + @SuppressWarnings("unchecked") + public MulticastPublisher(Executor executor, int bufferSize) { + if ((bufferSize & (bufferSize - 1)) != 0) { + throw new IllegalArgumentException("Please provide a power-of-two buffer size"); + } + this.executor = executor; + this.bufferSize = bufferSize; + subscribers.setRelease(EMPTY); + } + + public boolean offer(T item) { + Objects.requireNonNull(item, "item is null"); + + InnerSubscription[] a = subscribers.get(); + synchronized (this) { + for (InnerSubscription inner : a) { + if (inner.isFull()) { + return false; + } + } + for (InnerSubscription inner : a) { + inner.offer(item); + inner.drain(executor); + } + } + + return true; + } + + @SuppressWarnings("unchecked") + public void complete() { + if (done.compareAndSet(false, true)) { + for (InnerSubscription inner : subscribers.getAndSet(TERMINATED)) { + inner.done = true; + inner.drain(executor); + } + } + } + + @SuppressWarnings("unchecked") + public void completeExceptionally(Throwable error) { + if (done.compareAndSet(false, true)) { + this.error = error; + for (InnerSubscription inner : subscribers.getAndSet(TERMINATED)) { + inner.error = error; + inner.done = true; + inner.drain(executor); + } + } + } + + @Override + public void close() { + complete(); + } + + @Override + public void subscribe(Subscriber subscriber) { + Objects.requireNonNull(subscriber, "subscriber is null"); + InnerSubscription inner = new InnerSubscription(subscriber, bufferSize, this); + if (!add(inner)) { + Throwable ex = error; + if (ex != null) { + inner.error = ex; + } + inner.done = true; + } + inner.drain(executor); + } + + public boolean hasSubscribers() { + return subscribers.get().length != 0; + } + + boolean add(InnerSubscription inner) { + + for (;;) { + InnerSubscription[] a = subscribers.get(); + if (a == TERMINATED) { + return false; + } + + int n = a.length; + @SuppressWarnings("unchecked") + InnerSubscription[] b = new InnerSubscription[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = inner; + if (subscribers.compareAndSet(a, b)) { + return true; + } + } + } + + @SuppressWarnings("unchecked") + void remove(InnerSubscription inner) { + for (;;) { + InnerSubscription[] a = subscribers.get(); + int n = a.length; + if (n == 0) { + break; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == inner) { + j = i; + break; + } + } + if (j < 0) { + break; + } + InnerSubscription[] b; + if (n == 1) { + b = EMPTY; + } else { + b = new InnerSubscription[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (subscribers.compareAndSet(a, b)) { + break; + } + } + } + + static final class InnerSubscription implements Subscription, Runnable { + + final Subscriber actual; + final MulticastPublisher parent; + final AtomicReferenceArray queue; + final int mask; + + volatile boolean badRequest; + final AtomicBoolean cancelled = new AtomicBoolean(); + + volatile boolean done; + Throwable error; + + boolean subscribed; + long emitted; + + final AtomicLong requested = new AtomicLong(); + + final AtomicInteger wip = new AtomicInteger(); + + final AtomicLong producerIndex = new AtomicLong(); + + final AtomicLong consumerIndex = new AtomicLong(); + + InnerSubscription(Subscriber actual, int bufferSize, MulticastPublisher parent) { + this.actual = actual; + this.queue = new AtomicReferenceArray(bufferSize); + this.parent = parent; + this.mask = bufferSize - 1; + } + + void offer(T item) { + AtomicReferenceArray q = queue; + int m = mask; + long pi = producerIndex.get(); + int offset = (int)(pi) & m; + + q.setRelease(offset, item); + producerIndex.setRelease(pi + 1); + } + + T poll() { + AtomicReferenceArray q = queue; + int m = mask; + long ci = consumerIndex.get(); + + int offset = (int)(ci) & m; + T o = q.getAcquire(offset); + if (o != null) { + q.setRelease(offset, null); + consumerIndex.setRelease(ci + 1); + } + return o; + } + + boolean isFull() { + return 1 + mask + consumerIndex.get() == producerIndex.get(); + } + + void drain(Executor executor) { + if (wip.getAndAdd(1) == 0) { + executor.execute(this); + } + } + + @Override + public void request(long n) { + if (n <= 0L) { + badRequest = true; + done = true; + } else { + for (;;) { + long r = requested.get(); + long u = r + n; + if (u < 0) { + u = Long.MAX_VALUE; + } + if (requested.compareAndSet(r, u)) { + break; + } + } + } + drain(parent.executor); + } + + @Override + public void cancel() { + if (cancelled.compareAndSet(false, true)) { + parent.remove(this); + } + } + + void clear() { + error = null; + while (poll() != null) ; + } + + @Override + public void run() { + int missed = 1; + Subscriber a = actual; + + outer: + for (;;) { + + if (subscribed) { + if (cancelled.get()) { + clear(); + } else { + long r = requested.get(); + long e = emitted; + + while (e != r) { + if (cancelled.get()) { + continue outer; + } + + boolean d = done; + + if (d) { + Throwable ex = error; + if (ex != null) { + cancelled.setRelease(true); + a.onError(ex); + continue outer; + } + if (badRequest) { + cancelled.setRelease(true); + parent.remove(this); + a.onError(new IllegalArgumentException("§3.9 violated: request was not positive")); + continue outer; + } + } + + T v = poll(); + boolean empty = v == null; + + if (d && empty) { + cancelled.setRelease(true); + a.onComplete(); + break; + } + + if (empty) { + break; + } + + a.onNext(v); + + e++; + } + + if (e == r) { + if (cancelled.get()) { + continue outer; + } + if (done) { + Throwable ex = error; + if (ex != null) { + cancelled.setRelease(true); + a.onError(ex); + } else + if (badRequest) { + cancelled.setRelease(true); + a.onError(new IllegalArgumentException("§3.9 violated: request was not positive")); + } else + if (producerIndex == consumerIndex) { + cancelled.setRelease(true); + a.onComplete(); + } + } + } + + emitted = e; + } + } else { + subscribed = true; + a.onSubscribe(this); + } + + int w = wip.get(); + if (missed == w) { + w = wip.getAndAdd(-missed); + if (missed == w) { + break; + } + } + missed = w; + } + } + } +} \ No newline at end of file diff --git a/flow-bridge/src/test/java/org/reactivestreams/ReactiveStreamsFlowBridgeTest.java b/flow-bridge/src/test/java/org/reactivestreams/ReactiveStreamsFlowBridgeTest.java new file mode 100644 index 00000000..1e24172f --- /dev/null +++ b/flow-bridge/src/test/java/org/reactivestreams/ReactiveStreamsFlowBridgeTest.java @@ -0,0 +1,234 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.concurrent.Flow; +import java.util.concurrent.SubmissionPublisher; + +public class ReactiveStreamsFlowBridgeTest { + @Test + public void reactiveToFlowNormal() { + MulticastPublisher p = new MulticastPublisher(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, Flow.defaultBufferSize()); + + TestEitherConsumer tc = new TestEitherConsumer(); + + ReactiveStreamsFlowBridge.toFlowPublisher(p).subscribe(tc); + + p.offer(1); + p.offer(2); + p.offer(3); + p.offer(4); + p.offer(5); + p.complete(); + + tc.assertRange(1, 5); + } + + @Test + public void reactiveToFlowError() { + MulticastPublisher p = new MulticastPublisher(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, Flow.defaultBufferSize()); + + TestEitherConsumer tc = new TestEitherConsumer(); + + ReactiveStreamsFlowBridge.toFlowPublisher(p).subscribe(tc); + + p.offer(1); + p.offer(2); + p.offer(3); + p.offer(4); + p.offer(5); + p.completeExceptionally(new IOException()); + + tc.assertFailure(IOException.class, 1, 2, 3, 4, 5); + } + + @Test + public void flowToReactiveNormal() { + SubmissionPublisher p = new SubmissionPublisher(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, Flow.defaultBufferSize()); + + TestEitherConsumer tc = new TestEitherConsumer(); + + ReactiveStreamsFlowBridge.toPublisher(p).subscribe(tc); + + p.submit(1); + p.submit(2); + p.submit(3); + p.submit(4); + p.submit(5); + p.close(); + + tc.assertRange(1, 5); + } + + @Test + public void flowToReactiveError() { + SubmissionPublisher p = new SubmissionPublisher(new Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }, Flow.defaultBufferSize()); + + TestEitherConsumer tc = new TestEitherConsumer(); + + ReactiveStreamsFlowBridge.toPublisher(p).subscribe(tc); + + p.submit(1); + p.submit(2); + p.submit(3); + p.submit(4); + p.submit(5); + p.closeExceptionally(new IOException()); + + tc.assertFailure(IOException.class, 1, 2, 3, 4, 5); + } + + @Test + public void reactiveStreamsToFlowSubscriber() { + TestEitherConsumer tc = new TestEitherConsumer(); + + Flow.Subscriber fs = ReactiveStreamsFlowBridge.toFlowSubscriber(tc); + + final Object[] state = { null, null }; + + fs.onSubscribe(new Flow.Subscription() { + @Override + public void request(long n) { + state[0] = n; + } + + @Override + public void cancel() { + state[1] = true; + } + }); + + Assert.assertEquals(state[0], Long.MAX_VALUE); + + fs.onNext(1); + fs.onNext(2); + fs.onNext(3); + fs.onComplete(); + + tc.assertResult(1, 2, 3); + + Assert.assertNull(state[1]); + } + + @Test + public void flowToReactiveStreamsSubscriber() { + TestEitherConsumer tc = new TestEitherConsumer(); + + org.reactivestreams.Subscriber fs = ReactiveStreamsFlowBridge.toSubscriber(tc); + + final Object[] state = { null, null }; + + fs.onSubscribe(new org.reactivestreams.Subscription() { + @Override + public void request(long n) { + state[0] = n; + } + + @Override + public void cancel() { + state[1] = true; + } + }); + + Assert.assertEquals(state[0], Long.MAX_VALUE); + + fs.onNext(1); + fs.onNext(2); + fs.onNext(3); + fs.onComplete(); + + tc.assertResult(1, 2, 3); + + Assert.assertNull(state[1]); + } + + @Test + public void stableConversionForSubscriber() { + Subscriber rsSub = new Subscriber() { + @Override public void onSubscribe(Subscription s) {}; + @Override public void onNext(Integer i) {}; + @Override public void onError(Throwable t) {}; + @Override public void onComplete() {}; + }; + + Flow.Subscriber fSub = new Flow.Subscriber() { + @Override public void onSubscribe(Flow.Subscription s) {}; + @Override public void onNext(Integer i) {}; + @Override public void onError(Throwable t) {}; + @Override public void onComplete() {}; + }; + + Assert.assertSame(ReactiveStreamsFlowBridge.toSubscriber(ReactiveStreamsFlowBridge.toFlowSubscriber(rsSub)), rsSub); + Assert.assertSame(ReactiveStreamsFlowBridge.toFlowSubscriber(ReactiveStreamsFlowBridge.toSubscriber(fSub)), fSub); + } + + @Test + public void stableConversionForProcessor() { + Processor rsPro = new Processor() { + @Override public void onSubscribe(Subscription s) {}; + @Override public void onNext(Integer i) {}; + @Override public void onError(Throwable t) {}; + @Override public void onComplete() {}; + @Override public void subscribe(Subscriber s) {}; + }; + + Flow.Processor fPro = new Flow.Processor() { + @Override public void onSubscribe(Flow.Subscription s) {}; + @Override public void onNext(Integer i) {}; + @Override public void onError(Throwable t) {}; + @Override public void onComplete() {}; + @Override public void subscribe(Flow.Subscriber s) {}; + }; + + Assert.assertSame(ReactiveStreamsFlowBridge.toProcessor(ReactiveStreamsFlowBridge.toFlowProcessor(rsPro)), rsPro); + Assert.assertSame(ReactiveStreamsFlowBridge.toFlowProcessor(ReactiveStreamsFlowBridge.toProcessor(fPro)), fPro); + } + + @Test + public void stableConversionForPublisher() { + Publisher rsPub = new Publisher() { + @Override public void subscribe(Subscriber s) {}; + }; + + Flow.Publisher fPub = new Flow.Publisher() { + @Override public void subscribe(Flow.Subscriber s) {}; + }; + + Assert.assertSame(ReactiveStreamsFlowBridge.toPublisher(ReactiveStreamsFlowBridge.toFlowPublisher(rsPub)), rsPub); + Assert.assertSame(ReactiveStreamsFlowBridge.toFlowPublisher(ReactiveStreamsFlowBridge.toPublisher(fPub)), fPub); + } +} diff --git a/flow-bridge/src/test/java/org/reactivestreams/SubmissionPublisherTckTest.java b/flow-bridge/src/test/java/org/reactivestreams/SubmissionPublisherTckTest.java new file mode 100644 index 00000000..2c5b3a0d --- /dev/null +++ b/flow-bridge/src/test/java/org/reactivestreams/SubmissionPublisherTckTest.java @@ -0,0 +1,58 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams; + +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.concurrent.SubmissionPublisher; + +@Test +public class SubmissionPublisherTckTest extends PublisherVerification { + + public SubmissionPublisherTckTest() { + super(new TestEnvironment(300)); + } + + @Override + public Publisher createPublisher(final long elements) { + final SubmissionPublisher sp = new SubmissionPublisher(); + new Thread(new Runnable() { + @Override + public void run() { + while (!sp.hasSubscribers()) { + Thread.yield(); + } + for (int i = 0; i < elements; i++) { + sp.submit(i); + } + sp.close(); + } + }).start(); + return ReactiveStreamsFlowBridge.toPublisher(sp); + } + + @Override + public Publisher createFailedPublisher() { + final SubmissionPublisher sp = new SubmissionPublisher(); + sp.closeExceptionally(new IOException()); + return ReactiveStreamsFlowBridge.toPublisher(sp); + } + + @Override + public long maxElementsFromPublisher() { + return 100; + } + +} \ No newline at end of file diff --git a/flow-bridge/src/test/java/org/reactivestreams/TestEitherConsumer.java b/flow-bridge/src/test/java/org/reactivestreams/TestEitherConsumer.java new file mode 100644 index 00000000..5b8d8e63 --- /dev/null +++ b/flow-bridge/src/test/java/org/reactivestreams/TestEitherConsumer.java @@ -0,0 +1,174 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Flow; +import java.util.concurrent.TimeUnit; + +/** + * Class that provides basic state assertions on received elements and + * terminal signals from either a Reactive Streams Publisher or a + * Flow Publisher. + *

+ * As with standard {@link Subscriber}s, an instance of this class + * should be subscribed to at most one (Reactive Streams or + * Flow) Publisher. + *

+ * @param the element type + */ +class TestEitherConsumer implements Flow.Subscriber, Subscriber { + + protected final List values; + + protected final List errors; + + protected int completions; + + protected Flow.Subscription subscription; + + protected Subscription subscriptionRs; + + protected final CountDownLatch done; + + final long initialRequest; + + public TestEitherConsumer() { + this(Long.MAX_VALUE); + } + + public TestEitherConsumer(long initialRequest) { + this.values = new ArrayList(); + this.errors = new ArrayList(); + this.done = new CountDownLatch(1); + this.initialRequest = initialRequest; + } + + @Override + public final void onSubscribe(Flow.Subscription s) { + this.subscription = s; + s.request(initialRequest); + } + + @Override + public void onSubscribe(Subscription s) { + this.subscriptionRs = s; + s.request(initialRequest); + } + + @Override + public void onNext(T item) { + values.add(item); + if (subscription == null && subscriptionRs == null) { + errors.add(new IllegalStateException("onSubscribe not called")); + } + } + + @Override + public void onError(Throwable throwable) { + errors.add(throwable); + if (subscription == null && subscriptionRs == null) { + errors.add(new IllegalStateException("onSubscribe not called")); + } + done.countDown(); + } + + @Override + public void onComplete() { + completions++; + if (subscription == null && subscriptionRs == null) { + errors.add(new IllegalStateException("onSubscribe not called")); + } + done.countDown(); + } + + public final void cancel() { + // FIXME implement deferred cancellation + } + + public final List values() { + return values; + } + + public final List errors() { + return errors; + } + + public final int completions() { + return completions; + } + + public final boolean await(long timeout, TimeUnit unit) throws InterruptedException { + return done.await(timeout, unit); + } + + public final TestEitherConsumer assertResult(T... items) { + if (!values.equals(Arrays.asList(items))) { + throw new AssertionError("Expected: " + Arrays.toString(items) + ", Actual: " + values + ", Completions: " + completions); + } + if (completions != 1) { + throw new AssertionError("Not completed: " + completions); + } + return this; + } + + + public final TestEitherConsumer assertFailure(Class errorClass, T... items) { + if (!values.equals(Arrays.asList(items))) { + throw new AssertionError("Expected: " + Arrays.toString(items) + ", Actual: " + values + ", Completions: " + completions); + } + if (completions != 0) { + throw new AssertionError("Completed: " + completions); + } + if (errors.isEmpty()) { + throw new AssertionError("No errors"); + } + if (!errorClass.isInstance(errors.get(0))) { + AssertionError ae = new AssertionError("Wrong throwable"); + ae.initCause(errors.get(0)); + throw ae; + } + return this; + } + + public final TestEitherConsumer awaitDone(long timeout, TimeUnit unit) { + try { + if (!done.await(timeout, unit)) { + subscription.cancel(); + throw new RuntimeException("Timed out. Values: " + values.size() + + ", Errors: " + errors.size() + ", Completions: " + completions); + } + } catch (InterruptedException ex) { + throw new RuntimeException("Interrupted"); + } + return this; + } + + public final TestEitherConsumer assertRange(int start, int count) { + if (values.size() != count) { + throw new AssertionError("Expected: " + count + ", Actual: " + values.size()); + } + for (int i = 0; i < count; i++) { + if ((Integer)values.get(i) != start + i) { + throw new AssertionError("Index: " + i + ", Expected: " + + (i + start) + ", Actual: " +values.get(i)); + } + } + if (completions != 1) { + throw new AssertionError("Not completed: " + completions); + } + return this; + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 58385981..3030cc76 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 83cdb617..ff8278a9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 19 09:12:00 AST 2014 +#Sun Oct 29 16:12:50 JST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-bin.zip \ No newline at end of file +distributionUrl=http\://services.gradle.org/distributions/gradle-4.0.1-bin.zip diff --git a/gradlew b/gradlew index 91a7e269..cccdd3d5 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh ############################################################################## ## @@ -6,20 +6,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +48,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +59,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +89,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +113,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +154,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 8a0b282a..e95643d6 100755 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,90 +1,84 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle index 61cdbf6d..9272bcae 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,9 +1,39 @@ rootProject.name = 'reactive-streams' + +def jdkFlow = false + +final def ANSI_RESET = "\u001B[0m" +final def ANSI_RED = "\u001B[31m" +final def ANSI_GREEN = "\u001B[32m" +final def ANSI_YELLOW = "\u001B[33m" + +try { + Class.forName("java.util.concurrent.Flow") + jdkFlow = true + println(ANSI_GREEN + " INFO: ------------------ JDK9 classes detected ---------------------------------" + ANSI_RESET) + println(ANSI_GREEN + " INFO: Java 9 Flow API found; Including [flow-bridge, tck-flow] in build. " + ANSI_RESET) + println(ANSI_GREEN + " INFO: --------------------------------------------------------------------------" + ANSI_RESET) +} catch (Throwable ex) { + // Flow API not available + println(ANSI_RED + "WARNING: ------------------ JDK9 classes NOT detected -----------------------------" + ANSI_RESET) + println(ANSI_RED + "WARNING: Java 9 Flow API not found; Not including [flow-bridge, tck-flow] in build." + ANSI_RESET) + println(ANSI_RED + "WARNING: In order to execute the complete test-suite run the build using JDK9+. " + ANSI_RESET) + println(ANSI_RED + "WARNING: --------------------------------------------------------------------------" + ANSI_RESET) +} + include ':reactive-streams' include ':reactive-streams-tck' include ':reactive-streams-examples' +if (jdkFlow) { + include ':reactive-streams-flow-bridge' + include ':reactive-streams-tck-flow' +} + project(':reactive-streams').projectDir = "$rootDir/api" as File project(':reactive-streams-tck').projectDir = "$rootDir/tck" as File project(':reactive-streams-examples').projectDir = "$rootDir/examples" as File - +if (jdkFlow) { + project(':reactive-streams-flow-bridge').projectDir = "$rootDir/flow-bridge" as File + project(':reactive-streams-tck-flow').projectDir = "$rootDir/tck-flow" as File +} diff --git a/tck-flow/README.md b/tck-flow/README.md new file mode 100644 index 00000000..03a93a88 --- /dev/null +++ b/tck-flow/README.md @@ -0,0 +1,616 @@ +# Reactive Streams TCK for `java.util.concurrent.Flow.*` # + +The purpose of the *Reactive Streams Technology Compatibility Kit* (from here on referred to as: *the TCK*) is to guide +and help Reactive Streams library implementers to validate their implementations against the rules defined in [the Specification](https://github.com/reactive-streams/reactive-streams-jvm). + +Since this version of the TCK is intended to verify the interfaces contained in Java 9 (under `java.util.concurrent.Flow.*`), at least Java `9` is required to run this TCK. If you're looking for the previous TCK that was intended for Reactive Streams prior to their inclusion in the JDK please look at [] + +## Structure of the TCK + +The TCK aims to cover all rules defined in the Specification, however for some rules outlined in the Specification it is +not possible (or viable) to construct automated tests, thus the TCK can not claim to fully verify an implementation, however it is very helpful and is able to validate the most important rules. + +The TCK is split up into 4 TestNG test classes which are to be extended by implementers, providing their `Flow.Publisher` / `Flow.Subscriber` / `Flow.Processor` implementations for the test harness to validate. + +The tests are split in the following way: + +* `FlowPublisherVerification` +* `FlowSubscriberWhiteboxVerification` +* `FlowSubscriberBlackboxVerification` +* `IdentityFlowProcessorVerification` + +The sections below include examples on how these can be used and describe the various configuration options. + +The TCK is provided as binary artifact on [Maven Central](http://search.maven.org/#search|ga|1|reactive-streams-tck): + +```xml + + org.reactivestreams + reactive-streams-tck-flow + 1.0.1 + test + +``` + +Please refer to the [Reactive Streams Specification](https://github.com/reactive-streams/reactive-streams-jvm) for the current latest version number. Make sure that your Reactive Streams API and TCK dependency versions match. + +### Test method naming convention + +Since the TCK is aimed at Reactive Stream implementers, looking into the sources of the TCK is well expected and encouraged as it should help during the implementation cycle. + +In order to make mapping between test cases and Specification rules easier, each test case covering a specific +Specification rule abides the following naming convention: `TYPE_spec###_DESC` where: + +* `TYPE` is one of: [#type-required](required), [#type-optional](optional), [#type-stochastic](stochastic) or [#type-untested](untested) which describe if this test is covering a Rule that MUST or SHOULD be implemented. The specific words are explained in detail below. +* `###` is the Rule number (`1.xx` Rules are about `Publisher`s, `2.xx` Rules are about Subscribers etc.) +* `DESC` is a short explanation of what exactly is being tested in this test case, as sometimes one Rule may have multiple test cases in order to cover the entire Rule. + +Here is an example test method signature: + +```java + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#1.1 + @Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable { + // ... + } +``` + +#### Test types explained: + +```java +@Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable +``` + + +The `required_` means that this test case is a hard requirement, it covers a *MUST* or *MUST NOT* Rule of the Specification. + + +```java +@Test public void optional_spec104_mustSignalOnErrorWhenFails() throws Throwable +``` + + +The `optional_` means that this test case is an optional requirement, it covers a *MAY* or *SHOULD* Rule of the Specification. +This prefix is also used if more configuration is needed in order to run it, e.g. +`@Additional(implement = "createFailedPublisher") @Test` signals the implementer that in order to run this test +one has to implement the `Publisher createFailedPublisher()` method. + +```java +@Test public void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable +``` + + +The `stochastic_` means that the Rule is impossible or infeasible to deterministically verify— +usually this means that this test case can yield false positives ("be green") even if for some case, the given implementation may violate the tested behaviour. + +```java +@Test public void untested_spec106_mustConsiderSubscriptionCancelledAfterOnErrorOrOnCompleteHasBeenCalled() throws Throwable +``` + + +The `untested_` means that the test case is not implemented, either because it is inherently hard to verify (e.g. Rules which use +the wording "*SHOULD consider X as Y*"). Such tests will show up in your test runs as `SKIPPED`, with a message pointing out that the TCK is unable to validate this Rule. Solutions to deterministically test Rules which have been +marked with this prefix are most welcome – pull requests are encouraged! + +### Test isolation + +All test assertions are isolated within the required `TestEnvironment`, so it is safe to run the TCK tests in parallel. + +### Testing Publishers with restricted capabilities + +Some `Publisher`s will not be able to pass through all TCK tests due to some internal or fundamental decisions in their design. +For example, a `FuturePublisher` can be implemented such that it can only ever `onNext` **exactly once**—this means that it's not possible to run all TCK tests against it since some of them require multiple elements to be emitted. + +In order to allow such `Publisher`s to be tested against the spec's rules, the TCK provides the `maxElementsFromPublisher()` method as means of communicating the limited capabilities of the Publisher. For example, if a `Publisher` can only ever emit up to `2` elements, +tests in the TCK which require more than 2 elements to verify a rule will be skipped. + +In order to inform the TCK that the `Publisher` is only able to signal up to `2` elements, override the `maxElementsFromPublisher` method like this: + +```java +@Override public long maxElementsFromPublisher() { + return 2; +} +``` + +The TCK also supports `Publisher`s which are not able to signal completion. Imagine a `Publisher` being +backed by a timer—such a `Publisher` does not have a natural way to "complete" after some number of ticks. It would be +possible to implement a `Processor` which would "take n elements from the TickPublisher and then signal completion to the +downstream", but this adds a layer of indirection between the TCK and the `Publisher` one initially wanted to test. +It is suggested to test such unbouded `Publisher`s either way—using a "TakeNElementsProcessor" or by informing the TCK +that the `Publisher` is not able to signal completion. The TCK will then skip all tests which require `onComplete` signals to be emitted. + +In order to inform the TCK that your Publiher is not able to signal completion, override the `maxElementsFromPublisher` method like this: + +```java +@Override public long maxElementsFromPublisher() { + return publisherUnableToSignalOnComplete(); // == Long.MAX_VALUE == unbounded +} +``` + +### Testing a "failed" Publisher +The Reactive Streams Specification mandates certain behaviours for `Publisher`s which are "failed", +e.g. it was unable to initialize a connection it needs to emit elements. +It may be useful to specifically such known to be failed `Publisher` using the TCK. + +In order to run additional tests on a failed `Publisher` implement the `createFailedPublisher` method. +The expected behaviour from the returned implementation is to follow Rule 1.4 and Rule 1.9—which are concerned +with the order of emiting the `Subscription` and signaling the failure. + +```java +@Override public Flow.Publisher createFailedPublisher() { + final String invalidData = "this input string is known it to be failed"; + return new MyPublisher(invalidData); +} +``` + +In case there isn't a known up-front error state to put the `Publisher` into, +ignore these tests by returning `null` from the `createFailedPublisher` method. +It is important to remember that it is **illegal** to signal `onNext / onComplete / onError` before signalling the `Subscription` through `onSubscribe`, for details on this rule refer to the Reactive Streams specification. + +## Publisher Verification + +`FlowPublisherVerification` tests verify `Publisher` as well as some `Subscription` Rules of the Specification. + +In order to include it's tests in your test suite simply extend it, like this: + +```java +package com.example.streams; + +import org.reactivestreams.tck.flow.FlowPublisherVerification; +import org.reactivestreams.tck.TestEnvironment; + +import java.util.concurrent.Flow; + +public class RangePublisherTest extends FlowPublisherVerification { + + public RangePublisherTest() { + super(new TestEnvironment()); + } + + @Override + public Flow.Publisher createPublisher(long elements) { + return new RangePublisher(1, elements); + } + + @Override + public Flow.Publisher createFailedPublisher() { + return new Publisher() { + @Override + public void subscribe(Subscriber s) { + s.onError(new RuntimeException("Can't subscribe subscriber: " + s + ", because of reasons.")); + } + }; + } + + // ADDITIONAL CONFIGURATION + + @Override + public long maxElementsFromPublisher() { + return Long.MAX_VALUE - 1; + } + + @Override + public long boundedDepthOfOnNextAndRequestRecursion() { + return 1; + } +} +``` + +Notable configuration options include: + +* `maxElementsFromPublisher` – must be overridden in case the `Publisher` being tested is of bounded length, e.g. it's wrapping a `Future` and thus can only publish up to 1 element, in which case you + would return `1` from this method. It will cause all tests which require more elements in order to validate a certain + Rule to be skipped, +* `boundedDepthOfOnNextAndRequestRecursion` – which must be overridden when verifying synchronous `Publisher`s. + This number returned by this method will be used to validate if a `Subscription` adheres to Rule 3.3 and avoids "unbounded recursion". + +### Timeout configuration +Publisher tests make use of two kinds of timeouts, one is the `defaultTimeoutMillis` which corresponds to all methods used +within the TCK which await for something to happen. The other timeout is `publisherReferenceGCTimeoutMillis` which is only used in order to verify +[Rule 3.13](https://github.com/reactive-streams/reactive-streams-jvm#3.13) which defines that `Subscriber` references MUST be dropped +by the Publisher. + +Note that the TCK differenciates between timeouts for "waiting for a signal" (``defaultTimeoutMillis``), +and "asserting no signals happen during a given amount of time" (``envDefaultNoSignalsTimeoutMillis``). +While the latter defaults to the prior, it may be useful to tweak them independently when running on continious +integration servers (for example, keeping the no-signals timeout significantly lower). + +In order to configure these timeouts (for example when running on a slow continious integtation machine), you can either: + +**Use env variables** to set these timeouts, in which case the you can do: + +```bash +export DEFAULT_TIMEOUT_MILLIS=100 +export DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS=100 +export PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS=300 +``` + +Or **define the timeouts explicitly in code**: + +```java +public class RangePublisherTest extends FlowPublisherVerification { + + public static final long DEFAULT_TIMEOUT_MILLIS = 100L; + public static final long DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS = DEFAULT_TIMEOUT_MILLIS; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 500L; + + public RangePublisherTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + // ... +} +``` + +Note that explicitly passed in values take precedence over values provided by the environment + +## Subscriber Verification + +`Subscriber` Verification is split up into two files (styles) of tests. + +It is highly recommended to implement the `FlowSubscriberWhiteboxVerification` instead of the `FlowSubscriberBlackboxVerification` even if it is more work to do so, as it can test far more rules and corner cases in implementations that would otherwise be left untested—which is the case when using the Blackbox Verification. + +### createElement and Helper Publisher implementations +Since testing a `Subscriber` is not possible without a corresponding `Publisher` the TCK `Subscriber` Verifications +both provide a default "*helper publisher*" to drive its tests and also allow to replace this `Publisher` with a custom implementation. +The helper `Publisher` is an asynchronous `Publisher` by default—meaning that a `Subscriber` can not blindly assume single threaded execution. + +When extending `Subscriber` Verification classes a type parameter representing the element type passed through the stream must be given. +Implementations are typically not sensitive to the type of element being signalled, but sometimes a `Subscriber` may be limited to only be able to work within a known set of types - +like a `FileSubscriber extends Flow.Subscriber` for example, that writes each element (ByteBuffer) it receives into a file. +For element type agnostic Subscribers the simplest way is to parameterize the tests using `Integer` and in the `createElement(int idx)` method (explained below in futher detail), return the incoming `int`. +In case an implementation needs to work on a specific type, the verification class should be parameterized using that type (e.g. `class StringSubTest extends FlowSubscriberWhiteboxVerification`) and the `createElement` method must be overriden to return a `String`. + +While the Helper `Publisher` implementation is provided, creating its elements is not – this is because a given `Subscriber` +may for example only work with `HashedMessage` or some other specific kind of element. The TCK is unable to generate such +special messages automatically, so the TCK provides the `T createElement(Integer id)` method to be implemented as part of +`Subscriber` Verifications which should take the given `id` and return an element of type `T` (where `T` is the type of +elements flowing into the `Subscriber`, as known thanks to `... extends FlowSubscriberWhiteboxVerification`) representing +an element of the stream that will be passed on to the Subscriber. + +The simplest valid implemenation is to return the incoming `id` *as the element* in a verification using `Integer`s as +element types: + +```java +public class MySubscriberTest extends FlowSubscriberBlackboxVerification { + + // ... + + @Override + public Integer createElement(int element) { return element; } +} +``` + + +NOTE: The `createElement` method *MAY* be called *concurrently from multiple threads*. + +**Very advanced**: While it is not expected for many implementations having to do so, it is possible to take full control of the `Publisher` which will be driving the TCKs test. This can be achieved by implementing the `createHelperPublisher` method in which one can implement the `createHelperPublisher` method by returning a custom `Publisher` implementation which will then be used by the TCK to drive your `Subscriber` tests: + +```java +@Override public Flow.Publisher createHelperPublisher(long elements) { + return new Flow.Publisher() { /* CUSTOM IMPL HERE WHICH OF COURSE ALSO SHOULD PASS THE TCK */ }; +} +``` + + +### Subscriber Whitebox Verification + +The Whitebox Verification is able to verify most of the `Subscriber` Specification, at the additional cost that control over demand generation and cancellation must be handed over to the TCK via the `SubscriberPuppet`. + +Based on experience implementing the `SubscriberPuppet`—it can be tricky or even impossible for some implementations, +as such, not all implementations are expected to make use of the plain `FlowSubscriberWhiteboxVerification`, instead having to fall back to using the `FlowSubscriberBlackboxVerification`. + +For the simplest possible (and most common) `Subscriber` implementation using the whitebox verification boils down to +exteding (or delegating to) your implementation with additionally signalling and registering the test probe, as shown in the below example: + +```java +package com.example.streams; + +import org.reactivestreams.tck.flow.FlowSubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; + +import java.util.concurrent.Flow; + +public class MyFlowSubscriberWhiteboxVerificationTest extends FlowSubscriberWhiteboxVerification { + + public MyFlowSubscriberWhiteboxVerificationTest() { + super(new TestEnvironment()); + } + + // The implementation under test is "SyncSubscriber": + // class SyncSubscriber extends Flow.Subscriber { /* ... */ } + + @Override + public Flow.Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { + // in order to test the SyncSubscriber we must instrument it by extending it, + // and calling the WhiteboxSubscriberProbe in all of the Subscribers methods: + return new SyncSubscriber() { + @Override + public void onSubscribe(final Flow.Subscription s) { + super.onSubscribe(s); + + // register a successful Subscription, and create a Puppet, + // for the WhiteboxVerification to be able to drive its tests: + probe.registerOnSubscribe(new SubscriberPuppet() { + + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + } + + @Override + public void onNext(Integer element) { + // in addition to normal Subscriber work that you're testing, register onNext with the probe + super.onNext(element); + probe.registerOnNext(element); + } + + @Override + public void onError(Throwable cause) { + // in addition to normal Subscriber work that you're testing, register onError with the probe + super.onError(cause); + probe.registerOnError(cause); + } + + @Override + public void onComplete() { + // in addition to normal Subscriber work that you're testing, register onComplete with the probe + super.onComplete(); + probe.registerOnComplete(); + } + }; + } + + @Override + public Integer createElement(int element) { + return element; + } + +} +``` + +### Subscriber Blackbox Verification + +Blackbox Verification does not require anything besides providing a `Subscriber` and `Publisher` instances to the TCK, +at the expense of not being able to verify as much as the `FlowSubscriberWhiteboxVerification`: + +```java +package com.example.streams; + +import org.reactivestreams.tck.flow.FlowSubscriberBlackboxVerification; +import org.reactivestreams.tck.TestEnvironment; + +import java.util.concurrent.Flow; + +public class MyFlowSubscriberBlackboxVerificationTest extends FlowSubscriberBlackboxVerification { + + public MyFlowSubscriberBlackboxVerificationTest() { + super(new TestEnvironment()); + } + + @Override + public Flow.Subscriber createSubscriber() { + return new MySubscriber(); + } + + @Override + public Integer createElement(int element) { + return element; + } +} +``` + +### Timeout configuration +Similarily to `FlowPublisherVerification`, it is possible to set the timeouts used by the TCK to validate `Subscriber` behaviour either hard-coded or by using environment variables. + +**Use env variables** to set the timeout value to be used by the TCK: + +```bash +export DEFAULT_TIMEOUT_MILLIS=300 +``` + +Or **define the timeout explicitly in code**: + +```java +public class MySubscriberTest extends FlowSubscriberBlackboxVerification { + + public static final long DEFAULT_TIMEOUT_MILLIS = 300L; + + public RangePublisherTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS)); + } + + // ... +} +``` + +NOTE: hard-coded values *take precedence* over environment set values (!). + + +## Subscription Verification + +Please note that while `Subscription` does **not** have it's own test class, it's rules are validated inside of the +`Publisher` and `Subscriber` tests – depending if the Rule demands specific action to be taken by the publishing, or +subscribing side of the `Subscription` contract. + +## Identity Processor Verification + +An `IdentityFlowProcessorVerification` tests the given `Processor` for all `Subscriber`, `Publisher` as well as +`Subscription` rules (internally the `WhiteboxSubscriberVerification` is used for that). + +```java +package com.example.streams; + +import org.reactivestreams.tck.flow.IdentityFlowProcessorVerification; +import org.reactivestreams.tck.flow.FlowSubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; + +import java.util.concurrent.Flow; + +public class MyIdentityFlowProcessorVerificationTest extends IdentityFlowProcessorVerification { + + public static final long DEFAULT_TIMEOUT_MILLIS = 300L; + public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 1000L; + + + public MyIdentityFlowProcessorVerificationTest() { + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + } + + @Override + public Flow.Processor createIdentityProcessor(int bufferSize) { + return new MyIdentityProcessor(bufferSize); + } + + @Override + public Flow.Publisher createHelperPublisher(long elements) { + return new MyRangePublisher(1, elements); + } + + // ENABLE ADDITIONAL TESTS + + @Override + public Flow.Publisher createFailedPublisher() { + // return Publisher that only signals onError instead of null to run additional tests + // see this methods JavaDocs for more details on how the returned Publisher should work. + return null; + } + + // OPTIONAL CONFIGURATION OVERRIDES + // only override these if understanding the implications of doing so. + + @Override + public long maxElementsFromPublisher() { + return super.maxElementsFromPublisher(); + } + + @Override + public long boundedDepthOfOnNextAndRequestRecursion() { + return super.boundedDepthOfOnNextAndRequestRecursion(); + } +} +``` + +The additional configuration options reflect the options available in the `Subscriber` and `Publisher` Verifications. + +The `IdentityFlowProcessorVerification` also runs additional "sanity" verifications, which are not directly mapped to +Specification rules, but help to verify that a `Processor` won't "get stuck" or face similar problems. Please refer to the +sources for details on the tests included. + +## Ignoring tests +Since the tests are inherited instead of user defined it's not possible to use the usual `@Ignore` annotations +to skip certain tests (which may be perfectly reasonable if the implementation has some know constraints on what it +cannot implement). Below is a recommended pattern to skip tests inherited from the TCK's base classes: + +```java +package com.example.streams; + +import org.reactivestreams.tck.flow.IdentityFlowProcessorVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; + +import java.util.concurrent.Flow; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class MyIdentityProcessorTest extends IdentityFlowProcessorVerification { + + private ExecutorService e; + + @BeforeClass + public void before() { e = Executors.newFixedThreadPool(4); } + + @AfterClass + public void after() { if (e != null) e.shutdown(); } + + public SkippingIdentityProcessorTest() { + super(new TestEnvironment()); + } + + @Override + public ExecutorService publisherExecutorService() { + return e; + } + + @Override + public Integer createElement(int element) { + return element; + } + + @Override + public Flow.Processor createIdentityProcessor(int bufferSize) { + return new MyProcessor(bufferSize); // return implementation to be tested + } + + @Override + public Flow.Publisher createFailedPublisher() { + return null; // returning null means that the tests validating a failed publisher will be skipped + } + +} +``` + +## Which verifications must be implemented by a compliant implementation? +In order to be considered an Reactive Streams compliant require implementations to cover their +`Publisher`s and `Subscriber`s with TCK verifications. If a library only implements `Subscriber`s, it does not have to implement `Publisher` tests, the same applies to `IdentityFlowProcessorVerification`-it is not needed if the library does not contain `Processor`s. + +In the case of `Subscriber` Verification are two styles of verifications to available: Blackbox or Whitebox. +It is *strongly* recommend to test `Subscriber` implementations with the `FlowSubscriberWhiteboxVerification` as it is able to +verify most of the specification. The `FlowSubscriberBlackboxVerification` should only be used as a fallback, +once it's certain that implementing the whitebox version will not be possible—if that happens +feel free to open a ticket on the [reactive-streams-jvm](https://github.com/reactive-streams/reactive-streams-jvm) project explaining what made implementing the whitebox verification impossible. + +In summary: implementations are required to use Verifications for the parts of the Specification that they implement, +and encouraged to using the Whitebox Verification over Blackbox for `Subscriber` whenever possible. + +## Upgrading the TCK to newer versions +While it's not expected for the Reactive Streams Specification to change in the forseeable future, +it *may be* that some semantics may need to change at some point. In this case it should expected for test +methods being phased out in terms of deprecation or removal, new tests may also be added over time. + +In general this should not be of much concern, unless overriding test methods are overriden by implementers. +Implementers who find the need of overriding provided test methods are encouraged to reach out via opening Issues +on the [Reactive Streams](https://github.com/reactive-streams/reactive-streams-jvm) project, so the use case can be discussed and, most likely, the TCK improved. + +## Using the TCK from other programming languages + +The TCK was designed such that it should be possible to consume it using different JVM-based programming languages. +The section below shows how to use the TCK using different languages (contributions of examples for more languages are very welcome): + +### Scala + +In order to run the TCK using [ScalaTest](http://www.scalatest.org/) the test class must mix-in the `TestNGSuiteLike` trait (as of ScalaTest `2.2.x`). + +```scala +class IterablePublisherTest(env: TestEnvironment, publisherShutdownTimeout: Long) + extends FlowPublisherVerification[Int](env, publisherShutdownTimeout) + with TestNGSuiteLike { + + def this() { + this(new TestEnvironment(500), 1000) + } + + def createFlowPublisher(elements: Long): Flow.Publisher[Int] = ??? + + // example error state publisher implementation + override def createFailedFlowPublisher(): Flow.Publisher[Int] = + new Flow.Publisher[Int] { + override def subscribe(s: Flow.Subscriber[Int]): Unit = + s.onError(new Exception("Unable to serve subscribers right now!")) + } + +} +``` + +### Groovy, JRuby, Kotlin, others... + +Contributions to this document are very welcome! + +When implementing Reactive Streams using the TCK in some yet undocumented here, language, please feel free to share an example! diff --git a/tck-flow/build.gradle b/tck-flow/build.gradle new file mode 100644 index 00000000..17f93dc0 --- /dev/null +++ b/tck-flow/build.gradle @@ -0,0 +1,7 @@ +description = 'reactive-streams-tck-flow' +dependencies { + compile group: 'org.testng', name: 'testng', version:'5.14.10' + compile project(':reactive-streams-tck') + compile project(':reactive-streams-flow-bridge') +} +test.useTestNG() diff --git a/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowPublisherVerification.java b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowPublisherVerification.java new file mode 100644 index 00000000..fbfdd672 --- /dev/null +++ b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowPublisherVerification.java @@ -0,0 +1,63 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.Publisher; +import org.reactivestreams.ReactiveStreamsFlowBridge; +import org.reactivestreams.tck.PublisherVerification; +import org.reactivestreams.tck.TestEnvironment; + +import java.util.concurrent.Flow; + +/** + * Provides tests for verifying a Java 9+ {@link java.util.concurrent.Flow.Publisher} specification rules. + * + * @see java.util.concurrent.Flow.Publisher + */ +public abstract class FlowPublisherVerification extends PublisherVerification { + + public FlowPublisherVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis) { + super(env, publisherReferenceGCTimeoutMillis); + } + + public FlowPublisherVerification(TestEnvironment env) { + super(env); + } + + @Override + final public Publisher createPublisher(long elements) { + final Flow.Publisher flowPublisher = createFlowPublisher(elements); + return ReactiveStreamsFlowBridge.toPublisher(flowPublisher); + } + /** + * This is the main method you must implement in your test incarnation. + * It must create a Publisher for a stream with exactly the given number of elements. + * If `elements` is `Long.MAX_VALUE` the produced stream must be infinite. + */ + public abstract Flow.Publisher createFlowPublisher(long elements); + + @Override + final public Publisher createFailedPublisher() { + final Flow.Publisher failed = createFailedFlowPublisher(); + if (failed == null) return null; // because `null` means "SKIP" in createFailedPublisher + else return ReactiveStreamsFlowBridge.toPublisher(failed); + } + /** + * By implementing this method, additional TCK tests concerning a "failed" publishers will be run. + * + * The expected behaviour of the {@link Flow.Publisher} returned by this method is hand out a subscription, + * followed by signalling {@code onError} on it, as specified by Rule 1.9. + * + * If you ignore these additional tests, return {@code null} from this method. + */ + public abstract Flow.Publisher createFailedFlowPublisher(); +} diff --git a/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberBlackboxVerification.java b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberBlackboxVerification.java new file mode 100644 index 00000000..b9a4ca1b --- /dev/null +++ b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberBlackboxVerification.java @@ -0,0 +1,65 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.ReactiveStreamsFlowBridge; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.SubscriberBlackboxVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.support.SubscriberBlackboxVerificationRules; + +import java.util.concurrent.Flow; + +/** + * Provides tests for verifying {@link java.util.concurrent.Flow.Subscriber} and {@link java.util.concurrent.Flow.Subscription} + * specification rules, without any modifications to the tested implementation (also known as "Black Box" testing). + * + * This verification is NOT able to check many of the rules of the spec, and if you want more + * verification of your implementation you'll have to implement {@code org.reactivestreams.tck.SubscriberWhiteboxVerification} + * instead. + * + * @see java.util.concurrent.Flow.Subscriber + * @see java.util.concurrent.Flow.Subscription + */ +public abstract class FlowSubscriberBlackboxVerification extends SubscriberBlackboxVerification + implements SubscriberBlackboxVerificationRules { + + protected FlowSubscriberBlackboxVerification(TestEnvironment env) { + super(env); + } + + @Override + public final void triggerRequest(Subscriber subscriber) { + triggerFlowRequest(ReactiveStreamsFlowBridge.toFlowSubscriber(subscriber)); + } + /** + * Override this method if the {@link java.util.concurrent.Flow.Subscriber} implementation you are verifying + * needs an external signal before it signals demand to its Publisher. + * + * By default this method does nothing. + */ + public void triggerFlowRequest(Flow.Subscriber subscriber) { + // this method is intentionally left blank + } + + @Override + public final Subscriber createSubscriber() { + return ReactiveStreamsFlowBridge.toSubscriber(createFlowSubscriber()); + } + /** + * This is the main method you must implement in your test incarnation. + * It must create a new {@link Flow.Subscriber} instance to be subjected to the testing logic. + */ + abstract public Flow.Subscriber createFlowSubscriber(); + +} diff --git a/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberWhiteboxVerification.java b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberWhiteboxVerification.java new file mode 100644 index 00000000..23a74bd4 --- /dev/null +++ b/tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberWhiteboxVerification.java @@ -0,0 +1,48 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.ReactiveStreamsFlowBridge; +import org.reactivestreams.Subscriber; +import org.reactivestreams.tck.SubscriberWhiteboxVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; + +import java.util.concurrent.Flow; + +/** + * Provides whitebox style tests for verifying {@link java.util.concurrent.Flow.Subscriber} + * and {@link java.util.concurrent.Flow.Subscription} specification rules. + * + * @see java.util.concurrent.Flow.Subscriber + * @see java.util.concurrent.Flow.Subscription + */ +public abstract class FlowSubscriberWhiteboxVerification extends SubscriberWhiteboxVerification + implements SubscriberWhiteboxVerificationRules { + + protected FlowSubscriberWhiteboxVerification(TestEnvironment env) { + super(env); + } + + @Override + final public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { + return ReactiveStreamsFlowBridge.toSubscriber(createFlowSubscriber(probe)); + } + /** + * This is the main method you must implement in your test incarnation. + * It must create a new {@link org.reactivestreams.Subscriber} instance to be subjected to the testing logic. + * + * In order to be meaningfully testable your Subscriber must inform the given + * `WhiteboxSubscriberProbe` of the respective events having been received. + */ + protected abstract Flow.Subscriber createFlowSubscriber(WhiteboxSubscriberProbe probe); +} diff --git a/tck-flow/src/main/java/org/reactivestreams/tck/flow/IdentityFlowProcessorVerification.java b/tck-flow/src/main/java/org/reactivestreams/tck/flow/IdentityFlowProcessorVerification.java new file mode 100644 index 00000000..4e899afe --- /dev/null +++ b/tck-flow/src/main/java/org/reactivestreams/tck/flow/IdentityFlowProcessorVerification.java @@ -0,0 +1,72 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.tck.IdentityProcessorVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; +import org.reactivestreams.tck.flow.support.PublisherVerificationRules; + +public abstract class IdentityFlowProcessorVerification extends IdentityProcessorVerification + implements SubscriberWhiteboxVerificationRules, PublisherVerificationRules { + + public IdentityFlowProcessorVerification(TestEnvironment env) { + super(env); + } + + public IdentityFlowProcessorVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis) { + super(env, publisherReferenceGCTimeoutMillis); + } + + public IdentityFlowProcessorVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis, int processorBufferSize) { + super(env, publisherReferenceGCTimeoutMillis, processorBufferSize); + } + + protected abstract Publisher createFailedFlowPublisher(); + + protected abstract Processor createIdentityFlowProcessor(int bufferSize); + + protected abstract Subscriber createFlowSubscriber(FlowSubscriberWhiteboxVerification.WhiteboxSubscriberProbe probe); + + protected abstract Publisher createFlowHelperPublisher(long elements); + + protected abstract Publisher createFlowPublisher(long elements); + + @Override + public final Publisher createHelperPublisher(long elements) { + return createFlowHelperPublisher(elements); + } + + @Override + public final Processor createIdentityProcessor(int bufferSize) { + return createIdentityFlowProcessor(bufferSize); + } + + @Override + public final Publisher createFailedPublisher() { + return createFailedFlowPublisher(); + } + + @Override + public final Publisher createPublisher(long elements) { + return createFlowPublisher(elements); + } + + @Override + public final Subscriber createSubscriber(FlowSubscriberWhiteboxVerification.WhiteboxSubscriberProbe probe) { + return createFlowSubscriber(probe); + } + +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/EmptyLazyFlowPublisherTest.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/EmptyLazyFlowPublisherTest.java new file mode 100644 index 00000000..62b1fc24 --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/EmptyLazyFlowPublisherTest.java @@ -0,0 +1,58 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.ReactiveStreamsFlowBridge; +import org.reactivestreams.example.unicast.AsyncIterablePublisher; +import java.util.concurrent.Flow.Publisher; + +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Test +public class EmptyLazyFlowPublisherTest extends FlowPublisherVerification { + + private ExecutorService ex; + + public EmptyLazyFlowPublisherTest() { + super(new TestEnvironment()); + } + + @BeforeClass + void before() { ex = Executors.newFixedThreadPool(4); } + + @AfterClass + void after() { if (ex != null) ex.shutdown(); } + + @Override + public Publisher createFlowPublisher(long elements) { + return ReactiveStreamsFlowBridge.toFlowPublisher( + new AsyncIterablePublisher(Collections.emptyList(), ex) + ); + } + + @Override + public Publisher createFailedFlowPublisher() { + return null; + } + + @Override + public long maxElementsFromPublisher() { + return 0; + } +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/RangeFlowPublisherTest.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/RangeFlowPublisherTest.java new file mode 100644 index 00000000..e92e6b0a --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/RangeFlowPublisherTest.java @@ -0,0 +1,177 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Flow; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.*; + +@Test +public class RangeFlowPublisherTest extends FlowPublisherVerification { + + static final Map stacks = new ConcurrentHashMap(); + + static final Map states = new ConcurrentHashMap(); + + static final AtomicInteger id = new AtomicInteger(); + + @AfterClass + public static void afterClass() { + boolean fail = false; + StringBuilder b = new StringBuilder(); + for (Map.Entry t : states.entrySet()) { + if (!t.getValue()) { + b.append("\r\n-------------------------------"); + for (Object o : stacks.get(t.getKey())) { + b.append("\r\nat ").append(o); + } + fail = true; + } + } + if (fail) { + throw new AssertionError("Cancellations were missing:" + b); + } + } + + public RangeFlowPublisherTest() { + super(new TestEnvironment()); + } + + @Override + public Flow.Publisher createFlowPublisher(long elements) { + return new RangeFlowPublisher(1, elements); + } + + @Override + public Flow.Publisher createFailedFlowPublisher() { + return null; + } + + static final class RangeFlowPublisher + implements Flow.Publisher { + + final StackTraceElement[] stacktrace; + + final long start; + + final long count; + + RangeFlowPublisher(long start, long count) { + this.stacktrace = Thread.currentThread().getStackTrace(); + this.start = start; + this.count = count; + } + + @Override + public void subscribe(Flow.Subscriber s) { + if (s == null) { + throw new NullPointerException(); + } + + int ids = id.incrementAndGet(); + + RangeFlowSubscription parent = new RangeFlowSubscription(s, ids, start, start + count); + stacks.put(ids, stacktrace); + states.put(ids, false); + s.onSubscribe(parent); + } + + static final class RangeFlowSubscription extends AtomicLong implements Flow.Subscription { + + private static final long serialVersionUID = 9066221863682220604L; + + final Flow.Subscriber actual; + + final int ids; + + final long end; + + long index; + + volatile boolean cancelled; + + RangeFlowSubscription(Flow.Subscriber actual, int ids, long start, long end) { + this.actual = actual; + this.ids = ids; + this.index = start; + this.end = end; + } + + @Override + public void request(long n) { + if (!cancelled) { + if (n <= 0L) { + cancelled = true; + states.put(ids, true); + actual.onError(new IllegalArgumentException("§3.9 violated")); + return; + } + + for (;;) { + long r = get(); + long u = r + n; + if (u < 0L) { + u = Long.MAX_VALUE; + } + if (compareAndSet(r, u)) { + if (r == 0) { + break; + } + return; + } + } + + long idx = index; + long f = end; + + for (;;) { + long e = 0; + while (e != n && idx != f) { + if (cancelled) { + return; + } + + actual.onNext((int)idx); + + idx++; + e++; + } + + if (idx == f) { + if (!cancelled) { + states.put(ids, true); + actual.onComplete(); + } + return; + } + + index = idx; + n = addAndGet(-n); + if (n == 0) { + break; + } + } + } + } + + @Override + public void cancel() { + cancelled = true; + states.put(ids, true); + } + } + } +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/SingleElementFlowPublisherTest.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SingleElementFlowPublisherTest.java new file mode 100644 index 00000000..a58ee2ad --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SingleElementFlowPublisherTest.java @@ -0,0 +1,57 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import java.util.concurrent.Flow; +import java.util.concurrent.Flow.Publisher; + +import org.reactivestreams.ReactiveStreamsFlowBridge; +import org.reactivestreams.example.unicast.AsyncIterablePublisher; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.Collections; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Test +public class SingleElementFlowPublisherTest extends FlowPublisherVerification { + + private ExecutorService ex; + + public SingleElementFlowPublisherTest() { + super(new TestEnvironment()); + } + + @BeforeClass + void before() { ex = Executors.newFixedThreadPool(4); } + + @AfterClass + void after() { if (ex != null) ex.shutdown(); } + + @Override + public Flow.Publisher createFlowPublisher(long elements) { + return ReactiveStreamsFlowBridge.toFlowPublisher(new AsyncIterablePublisher(Collections.singleton(1), ex)); + } + + @Override + public Publisher createFailedFlowPublisher() { + return null; + } + + @Override + public long maxElementsFromPublisher() { + return 1; + } +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberTest.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberTest.java new file mode 100644 index 00000000..e9fa620b --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberTest.java @@ -0,0 +1,57 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.FlowSubscriberBlackboxVerification; +import org.reactivestreams.tck.flow.support.SyncTriggeredDemandFlowSubscriber; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; + +@Test // Must be here for TestNG to find and run this, do not remove +public class SyncTriggeredDemandSubscriberTest extends FlowSubscriberBlackboxVerification { + + private ExecutorService e; + @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } + @AfterClass void after() { if (e != null) e.shutdown(); } + + public SyncTriggeredDemandSubscriberTest() { + super(new TestEnvironment()); + } + + @Override + public void triggerFlowRequest(Flow.Subscriber subscriber) { + ((SyncTriggeredDemandFlowSubscriber) subscriber).triggerDemand(1); + } + + @Override public Flow.Subscriber createFlowSubscriber() { + return new SyncTriggeredDemandFlowSubscriber() { + private long acc; + @Override protected long foreach(final Integer element) { + acc += element; + return 1; + } + + @Override public void onComplete() { + } + }; + } + + @Override public Integer createElement(int element) { + return element; + } +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberWhiteboxTest.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberWhiteboxTest.java new file mode 100644 index 00000000..4b1d6284 --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberWhiteboxTest.java @@ -0,0 +1,86 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.TestEnvironment; +import org.reactivestreams.tck.flow.support.SyncTriggeredDemandFlowSubscriber; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Flow; + +@Test // Must be here for TestNG to find and run this, do not remove +public class SyncTriggeredDemandSubscriberWhiteboxTest extends FlowSubscriberWhiteboxVerification { + + private ExecutorService e; + @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } + @AfterClass void after() { if (e != null) e.shutdown(); } + + public SyncTriggeredDemandSubscriberWhiteboxTest() { + super(new TestEnvironment()); + } + + @Override + public Flow.Subscriber createFlowSubscriber(final WhiteboxSubscriberProbe probe) { + return new SyncTriggeredDemandFlowSubscriber() { + @Override + public void onSubscribe(final Flow.Subscription s) { + super.onSubscribe(s); + + probe.registerOnSubscribe(new SubscriberPuppet() { + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + } + + @Override + public void onNext(Integer element) { + super.onNext(element); + probe.registerOnNext(element); + } + + @Override + public void onError(Throwable cause) { + super.onError(cause); + probe.registerOnError(cause); + } + + @Override + public void onComplete() { + super.onComplete(); + probe.registerOnComplete(); + } + + @Override + protected long foreach(Integer element) { + return 1; + } + }; + } + + @Override public Integer createElement(int element) { + return element; + } + +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandFlowSubscriber.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandFlowSubscriber.java new file mode 100644 index 00000000..eccd0b2a --- /dev/null +++ b/tck-flow/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandFlowSubscriber.java @@ -0,0 +1,134 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +import java.util.concurrent.Flow; + +/** + * SyncTriggeredDemandSubscriber is an implementation of Reactive Streams `Subscriber`, + * it runs synchronously (on the Publisher's thread) and requests demand triggered from + * "the outside" using its `triggerDemand` method and from "the inside" using the return + * value of its user-defined `whenNext` method which is invoked to process each element. + * + * NOTE: The code below uses a lot of try-catches to show the reader where exceptions can be expected, and where they are forbidden. + */ +// FIXME, depend on the reactive streams version? but that's in test scope... +public abstract class SyncTriggeredDemandFlowSubscriber implements Flow.Subscriber { + private Flow.Subscription subscription; // Obeying rule 3.1, we make this private! + private boolean done = false; + + @Override public void onSubscribe(final Flow.Subscription s) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` + if (s == null) throw null; + + if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully + try { + s.cancel(); // Cancel the additional subscription + } catch(final Throwable t) { + //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 + (new IllegalStateException(s + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); + } + } else { + // We have to assign it locally before we use it, if we want to be a synchronous `Subscriber` + // Because according to rule 3.10, the Subscription is allowed to call `onNext` synchronously from within `request` + subscription = s; + } + } + + /** + * Requests the provided number of elements from the `Subscription` of this `Subscriber`. + * NOTE: This makes no attempt at thread safety so only invoke it once from the outside to initiate the demand. + * @return `true` if successful and `false` if not (either due to no `Subscription` or due to exceptions thrown) + */ + public boolean triggerDemand(final long n) { + final Flow.Subscription s = subscription; + if (s == null) return false; + else { + try { + s.request(n); + } catch(final Throwable t) { + // Subscription.request is not allowed to throw according to rule 3.16 + (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); + return false; + } + return true; + } + } + + @Override public void onNext(final T element) { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` + if (element == null) throw null; + + if (!done) { // If we aren't already done + try { + final long need = foreach(element); + if (need > 0) triggerDemand(need); + else if (need == 0) {} + else { + done(); + } + } catch (final Throwable t) { + done(); + try { + onError(t); + } catch (final Throwable t2) { + //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 + (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); + } + } + } + } + } + + // Showcases a convenience method to idempotently marking the Subscriber as "done", so we don't want to process more elements + // herefor we also need to cancel our `Subscription`. + private void done() { + //On this line we could add a guard against `!done`, but since rule 3.7 says that `Subscription.cancel()` is idempotent, we don't need to. + done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) + try { + subscription.cancel(); // Cancel the subscription + } catch(final Throwable t) { + //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 + (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); + } + } + + // This method is left as an exercise to the reader/extension point + // Don't forget to call `triggerDemand` at the end if you are interested in more data, + // a return value of < 0 indicates that the subscription should be cancelled, + // a value of 0 indicates that there is no current need, + // a value of > 0 indicates the current need. + protected abstract long foreach(final T element); + + @Override public void onError(final Throwable t) { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` + if (t == null) throw null; + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } + } + + @Override public void onComplete() { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); + } else { + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } + } +} diff --git a/tck-flow/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java b/tck-flow/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java new file mode 100644 index 00000000..e69de29b diff --git a/tck-flow/src/test/resources/testng.yaml b/tck-flow/src/test/resources/testng.yaml new file mode 100644 index 00000000..854a0ba0 --- /dev/null +++ b/tck-flow/src/test/resources/testng.yaml @@ -0,0 +1,10 @@ +name: TCKSuite +threadCount: 1 + +tests: + - name: TCK + classes: + - org.reactivestreams.tck.IdentityProcessorVerificationDelegationTest + - org.reactivestreams.tck.PublisherVerificationTest + - org.reactivestreams.tck.SubscriberBlackboxVerificationTest + - org.reactivestreams.tck.SubscriberWhiteboxVerificationTest diff --git a/tck/README.md b/tck/README.md index 6180e1f8..6baa655f 100644 --- a/tck/README.md +++ b/tck/README.md @@ -1,23 +1,25 @@ # Reactive Streams TCK # The purpose of the *Reactive Streams Technology Compatibility Kit* (from here on referred to as: *the TCK*) is to guide -and help Reactive Streams library implementers to validate their implementations against the rules defined in [the Specification](https://github.com/reactive-streams/reactive-streams). +and help Reactive Streams library implementers to validate their implementations against the rules defined in [the Specification](https://github.com/reactive-streams/reactive-streams-jvm). -The TCK is implemented using **plain Java (1.6)** and **TestNG** tests, and should be possible to use from other languages and testing libraries (such as Scala, Groovy, JRuby or others). +The TCK is implemented using **plain Java (1.6)** and **TestNG** tests, and should be possible to use from other JVM-based languages and testing libraries. ## Structure of the TCK The TCK aims to cover all rules defined in the Specification, however for some rules outlined in the Specification it is -not possible (or viable) to construct automated tests, thus the TCK does not claim to completely verify an implementation, however it is very helpful and is able to validate the most important rules. +not possible (or viable) to construct automated tests, thus the TCK can not claim to fully verify an implementation, however it is very helpful and is able to validate the most important rules. -The TCK is split up into 4 files JUnit 4 test classes which should be extended by implementers, providing their `Publisher` / `Subscriber` implementations for the test harness to validate them. The tests are split in the following way: +The TCK is split up into 4 TestNG test classes which are to be extended by implementers, providing their `Publisher` / `Subscriber` / `Processor` implementations for the test harness to validate. + +The tests are split in the following way: * `PublisherVerification` -* `SubscriberBlackboxVerification` * `SubscriberWhiteboxVerification` +* `SubscriberBlackboxVerification` * `IdentityProcessorVerification` -The next sections include examples on how these can be used and describe the various configuration options. +The sections below include examples on how these can be used and describe the various configuration options. The TCK is provided as binary artifact on [Maven Central](http://search.maven.org/#search|ga|1|reactive-streams-tck): @@ -25,60 +27,69 @@ The TCK is provided as binary artifact on [Maven Central](http://search.maven.or org.reactivestreams reactive-streams-tck - ... + 1.0.1 test ``` -Please refer to the [Reactive Streams Specification](https://github.com/reactive-streams/reactive-streams) for the current latest version number. Make sure that the API and TCK dependency versions are equal. +Please refer to the [Reactive Streams Specification](https://github.com/reactive-streams/reactive-streams-jvm) for the current latest version number. Make sure that your Reactive Streams API and TCK dependency versions match. -### Types of tests +### Test method naming convention -Since the TCK is aimed at Reactive Stream implementers, looking into the sources of the TCK is well expected, -and should help during a libraries implementation cycle. +Since the TCK is aimed at Reactive Stream implementers, looking into the sources of the TCK is well expected and encouraged as it should help during the implementation cycle. In order to make mapping between test cases and Specification rules easier, each test case covering a specific -Specification rule abides the following naming convention: `spec###_DESC` where: +Specification rule abides the following naming convention: `TYPE_spec###_DESC` where: -* `###` is the Rule number (`1.xx` Rules are about Publishers, `2.xx` Rules are about Subscribers etc.) +* `TYPE` is one of: [#type-required](required), [#type-optional](optional), [#type-stochastic](stochastic) or [#type-untested](untested) which describe if this test is covering a Rule that MUST or SHOULD be implemented. The specific words are explained in detail below. +* `###` is the Rule number (`1.xx` Rules are about `Publisher`s, `2.xx` Rules are about Subscribers etc.) * `DESC` is a short explanation of what exactly is being tested in this test case, as sometimes one Rule may have multiple test cases in order to cover the entire Rule. +Here is an example test method signature: + ```java - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.1 - @Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#1.1 + @Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable { // ... } ``` -The prefixes of the names of the test methods are used in order to signify the character of the test. For example, these are the kinds of prefixes you may find: -"`required_`", "`optional_`", "`stochastic_`", "`untested_`". - -Explanations: +#### Test types explained: ```java @Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable ``` -... means that this test case is a hard requirement, it covers a *MUST* or *MUST NOT* Rule of the Specification. + +The `required_` means that this test case is a hard requirement, it covers a *MUST* or *MUST NOT* Rule of the Specification. ```java @Test public void optional_spec104_mustSignalOnErrorWhenFails() throws Throwable ``` -... means that this test case is optional, it covers a *MAY* or *SHOULD* Rule of the Specification. This prefix is also used if more configuration is needed in order to run it, e.g. `@Additional(implement = "createErrorStatePublisher") @Test` signals the implementer that in order to include this test case in his test runs, (s)he must implement the `Publisher createErrorStatePublisher()` method. + +The `optional_` means that this test case is an optional requirement, it covers a *MAY* or *SHOULD* Rule of the Specification. +This prefix is also used if more configuration is needed in order to run it, e.g. +`@Additional(implement = "createFailedPublisher") @Test` signals the implementer that in order to run this test +one has to implement the `Publisher createFailedPublisher()` method. ```java @Test public void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable ``` -... means that the Rule is either racy, and/or inherently hard to verify without heavy modification of the tested implementation. Usually this means that this test case can yield false positives ("be green") even if for some case, the given implementation may violate the tested behaviour. + +The `stochastic_` means that the Rule is impossible or infeasible to deterministically verify— +usually this means that this test case can yield false positives ("be green") even if for some case, the given implementation may violate the tested behaviour. ```java @Test public void untested_spec106_mustConsiderSubscriptionCancelledAfterOnErrorOrOnCompleteHasBeenCalled() throws Throwable ``` -... means that the test case is not implemented, either because it is inherently hard to verify (e.g. Rules which use the wording "*SHOULD consider X as Y*") or have not been implemented yet (though we hope we have implemented all we could!). Such tests will show up in your test runs as `SKIPPED`, with a message pointing out that the TCK is unable to validate this Rule. We would be delighted if you can figure out a way to deterministically test Rules, which have been marked with this prefix – pull requests are very welcome! + +The `untested_` means that the test case is not implemented, either because it is inherently hard to verify (e.g. Rules which use +the wording "*SHOULD consider X as Y*"). Such tests will show up in your test runs as `SKIPPED`, with a message pointing out that the TCK is unable to validate this Rule. Solutions to deterministically test Rules which have been +marked with this prefix are most welcome – pull requests are encouraged! ### Test isolation @@ -86,15 +97,13 @@ All test assertions are isolated within the required `TestEnvironment`, so it is ### Testing Publishers with restricted capabilities -Some `Publisher`s will not be able to pass through all TCK tests due to some internal or fundamental decissions in their design. -For example, a `FuturePublisher` can be implemented such that it can only ever `onNext` **exactly once** - this means that it is not possible -to run all TCK tests against it, since the tests sometimes require multiple elements to be emitted. +Some `Publisher`s will not be able to pass through all TCK tests due to some internal or fundamental decisions in their design. +For example, a `FuturePublisher` can be implemented such that it can only ever `onNext` **exactly once**—this means that it's not possible to run all TCK tests against it since some of them require multiple elements to be emitted. -In order to allow such restricted capabilities to be tested against the spec's rules, the TCK provides the `maxElementsFromPublisher()` method -as means of communicating to the TCK the limited capabilities of the Publisher. For example, if a publisher can only ever emit up to `2` elements, -tests in the TCK which require more than 2 elements to verify a rule can be skipped. +In order to allow such `Publisher`s to be tested against the spec's rules, the TCK provides the `maxElementsFromPublisher()` method as means of communicating the limited capabilities of the Publisher. For example, if a `Publisher` can only ever emit up to `2` elements, +tests in the TCK which require more than 2 elements to verify a rule will be skipped. -In order to inform the TCK your Publisher is only able to signal up to `2` elements, override the `maxElementsFromPublisher` method like this: +In order to inform the TCK that the `Publisher` is only able to signal up to `2` elements, override the `maxElementsFromPublisher` method like this: ```java @Override public long maxElementsFromPublisher() { @@ -102,11 +111,12 @@ In order to inform the TCK your Publisher is only able to signal up to `2` eleme } ``` -The TCK also supports Publishers which are not able to signal completion. For example you might have a Publisher being backed by a timer. -Such Publisher does not have a natural way to "complete" after some number of ticks. It would be possible to implement a Processor which would -"take n elements from the TickPublisher and then signal completion to the downstream", but this adds a layer of indirection between the TCK and the -Publisher we initially wanted to test. We suggest testing such unbouded Publishers either way - using a "TakeNElementsProcessor" or by informing the TCK -that the publisher is not able to signal completion. The TCK will then skip all tests which require `onComplete` signals to be emitted. +The TCK also supports `Publisher`s which are not able to signal completion. Imagine a `Publisher` being +backed by a timer—such a `Publisher` does not have a natural way to "complete" after some number of ticks. It would be +possible to implement a `Processor` which would "take n elements from the TickPublisher and then signal completion to the +downstream", but this adds a layer of indirection between the TCK and the `Publisher` one initially wanted to test. +It is suggested to test such unbouded `Publisher`s either way—using a "TakeNElementsProcessor" or by informing the TCK +that the `Publisher` is not able to signal completion. The TCK will then skip all tests which require `onComplete` signals to be emitted. In order to inform the TCK that your Publiher is not able to signal completion, override the `maxElementsFromPublisher` method like this: @@ -116,9 +126,29 @@ In order to inform the TCK that your Publiher is not able to signal completion, } ``` +### Testing a "failed" Publisher +The Reactive Streams Specification mandates certain behaviours for `Publisher`s which are "failed", +e.g. it was unable to initialize a connection it needs to emit elements. +It may be useful to specifically such known to be failed `Publisher` using the TCK. + +In order to run additional tests on a failed `Publisher` implement the `createFailedPublisher` method. +The expected behaviour from the returned implementation is to follow Rule 1.4 and Rule 1.9—which are concerned +with the order of emiting the `Subscription` and signaling the failure. + +```java +@Override public Publisher createFailedPublisher() { + final String invalidData = "this input string is known it to be failed"; + return new MyPublisher(invalidData); +} +``` + +In case there isn't a known up-front error state to put the `Publisher` into, +ignore these tests by returning `null` from the `createFailedPublisher` method. +It is important to remember that it is **illegal** to signal `onNext / onComplete / onError` before signalling the `Subscription` through `onSubscribe`, for details on this rule refer to the Reactive Streams specification. + ## Publisher Verification -`PublisherVerification` tests verify Publisher as well as some Subscription Rules of the Specification. +`PublisherVerification` tests verify `Publisher` as well as some `Subscription` Rules of the Specification. In order to include it's tests in your test suite simply extend it, like this: @@ -142,7 +172,7 @@ public class RangePublisherTest extends PublisherVerification { } @Override - public Publisher createErrorStatePublisher() { + public Publisher createFailedPublisher() { return new Publisher() { @Override public void subscribe(Subscriber s) { @@ -155,7 +185,7 @@ public class RangePublisherTest extends PublisherVerification { @Override public long maxElementsFromPublisher() { - return Long.MAX_VALUE - 1; + return Long.MAX_VALUE—1; } @Override @@ -167,23 +197,31 @@ public class RangePublisherTest extends PublisherVerification { Notable configuration options include: -* `maxElementsFromPublisher` – which should only be overridden in case the Publisher under test is not able to provide arbitrary length streams, e.g. it's wrapping a `Future` and thus can only publish up to 1 element. In such case you should return `1` from this method. It will cause all tests which require more elements in order to validate a certain Rule to be skipped, -* `boundedDepthOfOnNextAndRequestRecursion` – which should only be overridden in case of synchronous Publishers. This number will be used to validate if a -`Subscription` actually solves the "unbounded recursion" problem (Rule 3.3). +* `maxElementsFromPublisher` – must be overridden in case the `Publisher` being tested is of bounded length, e.g. it's wrapping a `Future` and thus can only publish up to 1 element, in which case you + would return `1` from this method. It will cause all tests which require more elements in order to validate a certain + Rule to be skipped, +* `boundedDepthOfOnNextAndRequestRecursion` – which must be overridden when verifying synchronous `Publisher`s. + This number returned by this method will be used to validate if a `Subscription` adheres to Rule 3.3 and avoids "unbounded recursion". ### Timeout configuration Publisher tests make use of two kinds of timeouts, one is the `defaultTimeoutMillis` which corresponds to all methods used within the TCK which await for something to happen. The other timeout is `publisherReferenceGCTimeoutMillis` which is only used in order to verify -[Rule 3.13](https://github.com/reactive-streams/reactive-streams#3.13) which defines that subscriber references MUST be dropped +[Rule 3.13](https://github.com/reactive-streams/reactive-streams-jvm#3.13) which defines that `Subscriber` references MUST be dropped by the Publisher. +Note that the TCK differenciates between timeouts for "waiting for a signal" (``defaultTimeoutMillis``), +and "asserting no signals happen during a given amount of time" (``envDefaultNoSignalsTimeoutMillis``). +While the latter defaults to the prior, it may be useful to tweak them independently when running on continious +integration servers (for example, keeping the no-signals timeout significantly lower). + In order to configure these timeouts (for example when running on a slow continious integtation machine), you can either: -**Use env variables** to set these timeouts, in which case the you can just: +**Use env variables** to set these timeouts, in which case the you can do: ```bash -export DEFAULT_TIMEOUT_MILLIS=300 -export PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS=500 +export DEFAULT_TIMEOUT_MILLIS=100 +export DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS=100 +export PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS=300 ``` Or **define the timeouts explicitly in code**: @@ -191,11 +229,12 @@ Or **define the timeouts explicitly in code**: ```java public class RangePublisherTest extends PublisherVerification { - public static final long DEFAULT_TIMEOUT_MILLIS = 300L; + public static final long DEFAULT_TIMEOUT_MILLIS = 100L; + public static final long DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS = DEFAULT_TIMEOUT_MILLIS; public static final long PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS = 500L; public RangePublisherTest() { - super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_TIMEOUT_MILLIS), PUBLISHER_REFERENCE_CLEANUP_TIMEOUT_MILLIS); } // ... @@ -206,23 +245,30 @@ Note that explicitly passed in values take precedence over values provided by th ## Subscriber Verification -Subscriber rules Verification is split up into two files (styles) of tests. +`Subscriber` Verification is split up into two files (styles) of tests. -The Blackbox Verification tests do not require the implementation under test to be modified at all, yet they are *not* able to verify most rules. In Whitebox Verification, more control over `request()` calls etc. is required in order to validate rules more precisely. +It is highly recommended to implement the `SubscriberWhiteboxVerification` instead of the `SubscriberBlackboxVerification` even if it is more work to do so, as it can test far more rules and corner cases in implementations that would otherwise be left untested—which is the case when using the Blackbox Verification. ### createElement and Helper Publisher implementations -Since testing a `Subscriber` is not possible without a corresponding `Publisher` the TCK Subscriber Verifications -both provide a default "*helper publisher*" to drive its test and also alow to replace this Publisher with a custom implementation. -The helper publisher is an asynchronous publisher by default - meaning that your subscriber can not blindly assume single threaded execution. - -While the `Publisher` implementation is provided, creating the signal elements is not – this is because a given Subscriber -may for example only work with `HashedMessage` or some other specific kind of signal. The TCK is unable to generate such -special messages automatically, so we provide the `T createElement(Integer id)` method to be implemented as part of -Subscriber Verifications which should take the given ID and return an element of type `T` (where `T` is the type of -elements flowing into the `Subscriber`, as known thanks to `... extends WhiteboxSubscriberVerification`) representing +Since testing a `Subscriber` is not possible without a corresponding `Publisher` the TCK `Subscriber` Verifications +both provide a default "*helper publisher*" to drive its tests and also allow to replace this `Publisher` with a custom implementation. +The helper `Publisher` is an asynchronous `Publisher` by default—meaning that a `Subscriber` can not blindly assume single threaded execution. + +When extending `Subscriber` Verification classes a type parameter representing the element type passed through the stream must be given. +Implementations are typically not sensitive to the type of element being signalled, but sometimes a `Subscriber` may be limited to only be able to work within a known set of types - +like a `FileSubscriber extends Subscriber` for example, that writes each element (ByteBuffer) it receives into a file. +For element type agnostic Subscribers the simplest way is to parameterize the tests using `Integer` and in the `createElement(int idx)` method (explained below in futher detail), return the incoming `int`. +In case an implementation needs to work on a specific type, the verification class should be parameterized using that type (e.g. `class StringSubTest extends SubscriberWhiteboxVerification`) and the `createElement` method must be overriden to return a `String`. + +While the Helper `Publisher` implementation is provided, creating its elements is not – this is because a given `Subscriber` +may for example only work with `HashedMessage` or some other specific kind of element. The TCK is unable to generate such +special messages automatically, so the TCK provides the `T createElement(Integer id)` method to be implemented as part of +`Subscriber` Verifications which should take the given `id` and return an element of type `T` (where `T` is the type of +elements flowing into the `Subscriber`, as known thanks to `... extends SubscriberWhiteboxVerification`) representing an element of the stream that will be passed on to the Subscriber. -The simplest valid implemenation is to return the incoming `id` *as the element* in a verification using `Integer`s as element types: +The simplest valid implemenation is to return the incoming `id` *as the element* in a verification using `Integer`s as +element types: ```java public class MySubscriberTest extends SubscriberBlackboxVerification { @@ -235,60 +281,26 @@ public class MySubscriberTest extends SubscriberBlackboxVerification { ``` -The `createElement` method MAY be called from multiple -threads, so in case of more complicated implementations, please be aware of this fact. +NOTE: The `createElement` method *MAY* be called *concurrently from multiple threads*. -**Very Advanced**: While we do not expect many implementations having to do so, it is possible to take full control of the `Publisher` -which will be driving the TCKs test. You can do this by implementing the `createHelperPublisher` method in which you can implement your -own Publisher which will then be used by the TCK to drive your Subscriber tests: +**Very advanced**: While it is not expected for many implementations having to do so, it is possible to take full control of the `Publisher` which will be driving the TCKs test. This can be achieved by implementing the `createHelperPublisher` method in which one can implement the `createHelperPublisher` method by returning a custom `Publisher` implementation which will then be used by the TCK to drive your `Subscriber` tests: ```java @Override public Publisher createHelperPublisher(long elements) { - return new Publisher() { /* IMPL HERE */ }; -} -``` - -### Subscriber Blackbox Verification - -Blackbox Verification does not require any additional work except from providing a `Subscriber` and `Publisher` instances to the TCK: - -```java -package com.example.streams; - -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import org.reactivestreams.tck.SubscriberBlackboxVerification; -import org.reactivestreams.tck.TestEnvironment; - -public class MySubscriberBlackboxVerificationTest extends SubscriberBlackboxVerification { - - public MySubscriberBlackboxVerificationTest() { - super(new TestEnvironment()); - } - - @Override - public Subscriber createSubscriber() { - return new MySubscriber(); - } - - @Override - public Integer createElement(int element) { - return element; - } + return new Publisher() { /* CUSTOM IMPL HERE WHICH OF COURSE ALSO SHOULD PASS THE TCK */ }; } ``` ### Subscriber Whitebox Verification -The Whitebox Verification tests are able to verify most of the Specification, at the additional cost that control over demand generation and cancellation must be handed over to the TCK via the `SubscriberPuppet`. +The Whitebox Verification is able to verify most of the `Subscriber` Specification, at the additional cost that control over demand generation and cancellation must be handed over to the TCK via the `SubscriberPuppet`. -Based on experiences so far implementing the `SubscriberPuppet` is non-trivial and can be hard for some implementations. -We keep the whitebox verification, as it is tremendously useful in the `ProcessorVerification`, where the Puppet is implemented within the TCK and injected to the tests. -We do not expect all implementations to make use of the plain `SubscriberWhiteboxVerification`, using the `SubscriberBlackboxVerification` instead. +Based on experience implementing the `SubscriberPuppet`—it can be tricky or even impossible for some implementations, +as such, not all implementations are expected to make use of the plain `SubscriberWhiteboxVerification`, instead having to fall back to using the `SubscriberBlackboxVerification`. -A simple synchronous `Subscriber` implementation would look similar to following example: +For the simplest possible (and most common) `Subscriber` implementation using the whitebox verification boils down to +exteding (or delegating to) your implementation with additionally signalling and registering the test probe, as shown in the below example: ```java package com.example.streams; @@ -305,21 +317,25 @@ public class MySubscriberWhiteboxVerificationTest extends SubscriberWhiteboxVeri super(new TestEnvironment()); } + // The implementation under test is "SyncSubscriber": + // class SyncSubscriber extends Subscriber { /* ... */ } + @Override public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { - - // return YOUR subscriber under-test, with additional WhiteboxSubscriberProbe instrumentation - return new Subscriber() { - + // in order to test the SyncSubscriber we must instrument it by extending it, + // and calling the WhiteboxSubscriberProbe in all of the Subscribers methods: + return new SyncSubscriber() { @Override public void onSubscribe(final Subscription s) { - // in addition to normal Subscriber work that you're testing, - // register a SubscriberPuppet, to give the TCK control over demand generation and cancelling + super.onSubscribe(s); + + // register a successful Subscription, and create a Puppet, + // for the WhiteboxVerification to be able to drive its tests: probe.registerOnSubscribe(new SubscriberPuppet() { @Override - public void triggerRequest(long n) { - s.request(n); + public void triggerRequest(long elements) { + s.request(elements); } @Override @@ -330,20 +346,23 @@ public class MySubscriberWhiteboxVerificationTest extends SubscriberWhiteboxVeri } @Override - public void onNext(Integer value) { + public void onNext(Integer element) { // in addition to normal Subscriber work that you're testing, register onNext with the probe - probe.registerOnNext(value); + super.onNext(element); + probe.registerOnNext(element); } @Override public void onError(Throwable cause) { // in addition to normal Subscriber work that you're testing, register onError with the probe + super.onError(cause); probe.registerOnError(cause); } @Override public void onComplete() { // in addition to normal Subscriber work that you're testing, register onComplete with the probe + super.onComplete(); probe.registerOnComplete(); } }; @@ -357,9 +376,40 @@ public class MySubscriberWhiteboxVerificationTest extends SubscriberWhiteboxVeri } ``` +### Subscriber Blackbox Verification + +Blackbox Verification does not require anything besides providing a `Subscriber` and `Publisher` instances to the TCK, +at the expense of not being able to verify as much as the `SubscriberWhiteboxVerification`: + +```java +package com.example.streams; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.reactivestreams.tck.SubscriberBlackboxVerification; +import org.reactivestreams.tck.TestEnvironment; + +public class MySubscriberBlackboxVerificationTest extends SubscriberBlackboxVerification { + + public MySubscriberBlackboxVerificationTest() { + super(new TestEnvironment()); + } + + @Override + public Subscriber createSubscriber() { + return new MySubscriber(); + } + + @Override + public Integer createElement(int element) { + return element; + } +} +``` + ### Timeout configuration -Similarily to `PublisherVerification`, it is possible to set the timeouts used by the TCK to validate subscriber behaviour. -This can be set either by using env variables or hardcoded explicitly. +Similarily to `PublisherVerification`, it is possible to set the timeouts used by the TCK to validate `Subscriber` behaviour either hard-coded or by using environment variables. **Use env variables** to set the timeout value to be used by the TCK: @@ -370,28 +420,31 @@ export DEFAULT_TIMEOUT_MILLIS=300 Or **define the timeout explicitly in code**: ```java -public class MySubscriberTest extends BlackboxSubscriberVerification { +public class MySubscriberTest extends SubscriberBlackboxVerification { public static final long DEFAULT_TIMEOUT_MILLIS = 300L; public RangePublisherTest() { - super(new MySubscriberTest(DEFAULT_TIMEOUT_MILLIS)); + super(new TestEnvironment(DEFAULT_TIMEOUT_MILLIS)); } // ... } ``` -Note that hard-coded values *take precedence* over environment set values (!). +NOTE: hard-coded values *take precedence* over environment set values (!). ## Subscription Verification -Please note that while `Subscription` does **not** have it's own test class, it's rules are validated inside of the `Publisher` and `Subscriber` tests – depending if the Rule demands specific action to be taken by the publishing, or subscribing side of the subscription contract. +Please note that while `Subscription` does **not** have it's own test class, it's rules are validated inside of the +`Publisher` and `Subscriber` tests – depending if the Rule demands specific action to be taken by the publishing, or +subscribing side of the `Subscription` contract. ## Identity Processor Verification -An `IdentityProcessorVerification` tests the given `Processor` for all `Subscriber`, `Publisher` as well as `Subscription` rules. Internally the `WhiteboxSubscriberVerification` is used for `Subscriber` rules. +An `IdentityProcessorVerification` tests the given `Processor` for all `Subscriber`, `Publisher` as well as +`Subscription` rules (internally the `WhiteboxSubscriberVerification` is used for that). ```java package com.example.streams; @@ -427,12 +480,14 @@ public class MyIdentityProcessorVerificationTest extends IdentityProcessorVerifi // ENABLE ADDITIONAL TESTS @Override - public Publisher createErrorStatePublisher() { - // return error state Publisher instead of null to run additional tests + public Publisher createFailedPublisher() { + // return Publisher that only signals onError instead of null to run additional tests + // see this methods JavaDocs for more details on how the returned Publisher should work. return null; } // OPTIONAL CONFIGURATION OVERRIDES + // only override these if understanding the implications of doing so. @Override public long maxElementsFromPublisher() { @@ -446,46 +501,94 @@ public class MyIdentityProcessorVerificationTest extends IdentityProcessorVerifi } ``` -The additional configuration options reflect the options available in the Subscriber and Publisher Verifications. +The additional configuration options reflect the options available in the `Subscriber` and `Publisher` Verifications. -The `IdentityProcessorVerification` also runs additional sanity verifications, which are not directly mapped to Specification rules, but help to verify that a Processor won't "get stuck" or face similar problems. Please refer to the sources for details on the tests included. +The `IdentityProcessorVerification` also runs additional "sanity" verifications, which are not directly mapped to +Specification rules, but help to verify that a `Processor` won't "get stuck" or face similar problems. Please refer to the +sources for details on the tests included. ## Ignoring tests -Since you inherit these tests instead of defining them yourself it's not possible to use the usual `@Ignore` annotations to skip certain tests -(which may be perfectly reasonable if your implementation has some know constraints on what it cannot implement). We recommend the below pattern -to skip tests inherited from the TCK's base classes: +Since the tests are inherited instead of user defined it's not possible to use the usual `@Ignore` annotations +to skip certain tests (which may be perfectly reasonable if the implementation has some know constraints on what it +cannot implement). Below is a recommended pattern to skip tests inherited from the TCK's base classes: ```java package com.example.streams; +import org.reactivestreams.Processor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.reactivestreams.tck.IdentityProcessorVerification; +import org.reactivestreams.tck.TestEnvironment; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class MyIdentityProcessorTest extends IdentityProcessorVerification { -public class SkippingIdentityProcessorTest extends IdentityProcessorVerification { + private ExecutorService e; + + @BeforeClass + public void before() { e = Executors.newFixedThreadPool(4); } + + @AfterClass + public void after() { if (e != null) e.shutdown(); } public SkippingIdentityProcessorTest() { - super(new TestEnvironment(500, true), 1000); + super(new TestEnvironment()); + } + + @Override + public ExecutorService publisherExecutorService() { + return e; + } + + @Override + public Integer createElement(int element) { + return element; } @Override public Processor createIdentityProcessor(int bufferSize) { - return /* ... */; + return new MyProcessor(bufferSize); // return implementation to be tested } - @Override // override the test method, and provide a reason on why you're doing so in the notVerified() message - public void spec999_mustDoVeryCrazyThings() throws Throwable { - notVerified("Unable to implement test because ..."); + @Override + public Publisher createFailedPublisher() { + return null; // returning null means that the tests validating a failed publisher will be skipped } } ``` -## Upgrade story +## Which verifications must be implemented by a compliant implementation? +In order to be considered an Reactive Streams compliant require implementations to cover their +`Publisher`s and `Subscriber`s with TCK verifications. If a library only implements `Subscriber`s, it does not have to implement `Publisher` tests, the same applies to `IdentityProcessorVerification`-it is not needed if the library does not contain `Processor`s. + +In the case of `Subscriber` Verification are two styles of verifications to available: Blackbox or Whitebox. +It is *strongly* recommend to test `Subscriber` implementations with the `SubscriberWhiteboxVerification` as it is able to +verify most of the specification. The `SubscriberBlackboxVerification` should only be used as a fallback, +once it's certain that implementing the whitebox version will not be possible—if that happens +feel free to open a ticket on the [reactive-streams-jvm](https://github.com/reactive-streams/reactive-streams-jvm) project explaining what made implementing the whitebox verification impossible. -**TODO** - What is our story about updating the TCK? How do we make sure that implementations don't accidentally miss some change in the spec, if the TCK is unable to fail verify the new behavior? Comments are very welcome, discussion about this is under-way in [Issue #99 – TCK Upgrade Story](https://github.com/reactive-streams/reactive-streams/issues/99). +In summary: implementations are required to use Verifications for the parts of the Specification that they implement, +and encouraged to using the Whitebox Verification over Blackbox for `Subscriber` whenever possible. -## Using the TCK from other languages +## Upgrading the TCK to newer versions +While it's not expected for the Reactive Streams Specification to change in the forseeable future, +it *may be* that some semantics may need to change at some point. In this case it should expected for test +methods being phased out in terms of deprecation or removal, new tests may also be added over time. -The TCK was designed such that it should be possible to consume it using different languages. +In general this should not be of much concern, unless overriding test methods are overriden by implementers. +Implementers who find the need of overriding provided test methods are encouraged to reach out via opening Issues +on the [Reactive Streams](https://github.com/reactive-streams/reactive-streams-jvm) project, so the use case can be discussed and, most likely, the TCK improved. + +## Using the TCK from other programming languages + +The TCK was designed such that it should be possible to consume it using different JVM-based programming languages. The section below shows how to use the TCK using different languages (contributions of examples for more languages are very welcome): ### Scala @@ -504,11 +607,10 @@ class IterablePublisherTest(env: TestEnvironment, publisherShutdownTimeout: Long def createPublisher(elements: Long): Publisher[Int] = ??? // example error state publisher implementation - override def createErrorStatePublisher(): Publisher[Int] = + override def createFailedPublisher(): Publisher[Int] = new Publisher[Int] { - override def subscribe(s: Subscriber[Int]): Unit = { + override def subscribe(s: Subscriber[Int]): Unit = s.onError(new Exception("Unable to serve subscribers right now!")) - } } } @@ -518,4 +620,4 @@ class IterablePublisherTest(env: TestEnvironment, publisherShutdownTimeout: Long Contributions to this document are very welcome! -If you're implementing reactive streams using the TCK in some language, please feel free to share an example on how to best use it from your language of choice. +When implementing Reactive Streams using the TCK in some yet undocumented here, language, please feel free to share an example! diff --git a/tck/src/main/java/org/reactivestreams/tck/IdentityProcessorVerification.java b/tck/src/main/java/org/reactivestreams/tck/IdentityProcessorVerification.java index f2774795..2fac7fe7 100644 --- a/tck/src/main/java/org/reactivestreams/tck/IdentityProcessorVerification.java +++ b/tck/src/main/java/org/reactivestreams/tck/IdentityProcessorVerification.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Processor; @@ -8,16 +19,16 @@ import org.reactivestreams.tck.TestEnvironment.ManualSubscriber; import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport; import org.reactivestreams.tck.TestEnvironment.Promise; -import org.reactivestreams.tck.support.Function; -import org.reactivestreams.tck.support.SubscriberWhiteboxVerificationRules; -import org.reactivestreams.tck.support.PublisherVerificationRules; +import org.reactivestreams.tck.flow.support.Function; +import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; +import org.reactivestreams.tck.flow.support.PublisherVerificationRules; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import java.util.HashSet; import java.util.Set; -public abstract class IdentityProcessorVerification extends WithHelperPublisher +public abstract class IdentityProcessorVerification extends WithHelperPublisher implements SubscriberWhiteboxVerificationRules, PublisherVerificationRules { private final TestEnvironment env; @@ -94,8 +105,8 @@ public Publisher createPublisher(long elements) { } @Override - public Publisher createErrorStatePublisher() { - return IdentityProcessorVerification.this.createErrorStatePublisher(); + public Publisher createFailedPublisher() { + return IdentityProcessorVerification.this.createFailedPublisher(); } @Override @@ -125,10 +136,14 @@ public boolean skipStochasticTests() { public abstract Processor createIdentityProcessor(int bufferSize); /** - * Return a Publisher that immediately signals {@code onError} to incoming subscriptions, - * or {@code null} in order to skip them. + * By implementing this method, additional TCK tests concerning a "failed" publishers will be run. + * + * The expected behaviour of the {@link Publisher} returned by this method is hand out a subscription, + * followed by signalling {@code onError} on it, as specified by Rule 1.9. + * + * If you ignore these additional tests, return {@code null} from this method. */ - public abstract Publisher createErrorStatePublisher(); + public abstract Publisher createFailedPublisher(); /** * Override and return lower value if your Publisher is only able to produce a known number of elements. @@ -148,7 +163,7 @@ public long maxElementsFromPublisher() { * {@code Subscription} actually solves the "unbounded recursion" problem by not allowing the number of * recursive calls to exceed the number returned by this method. * - * @see reactive streams spec, rule 3.3 + * @see reactive streams spec, rule 3.3 * @see PublisherVerification#required_spec303_mustNotAllowUnboundedRecursion() */ public long boundedDepthOfOnNextAndRequestRecursion() { @@ -204,7 +219,7 @@ public void required_validate_boundedDepthOfOnNextAndRequestRecursion() throws E } /////////////////////// DELEGATED TESTS, A PROCESSOR "IS A" PUBLISHER ////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#4.1 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#4.1 @Test public void required_createPublisher1MustProduceAStreamOfExactly1Element() throws Throwable { @@ -271,6 +286,21 @@ public void untested_spec109_subscribeShouldNotThrowNonFatalThrowable() throws T publisherVerification.untested_spec109_subscribeShouldNotThrowNonFatalThrowable(); } + @Override @Test + public void required_spec109_subscribeThrowNPEOnNullSubscriber() throws Throwable { + publisherVerification.required_spec109_subscribeThrowNPEOnNullSubscriber(); + } + + @Override @Test + public void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe() throws Throwable { + publisherVerification.required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe(); + } + + @Override @Test + public void required_spec109_mustIssueOnSubscribeForNonNullSubscriber() throws Throwable { + publisherVerification.required_spec109_mustIssueOnSubscribeForNonNullSubscriber(); + } + @Override @Test public void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice() throws Throwable { publisherVerification.untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice(); @@ -282,23 +312,23 @@ public void optional_spec111_maySupportMultiSubscribe() throws Throwable { } @Override @Test - public void required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe() throws Throwable { - publisherVerification.required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe(); + public void optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals() throws Throwable { + publisherVerification.optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals(); } @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable { - publisherVerification.required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne(); + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable { + publisherVerification.optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne(); } @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable { - publisherVerification.required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront(); + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable { + publisherVerification.optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront(); } @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable { - publisherVerification.required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected(); + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable { + publisherVerification.optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected(); } @Override @Test @@ -317,8 +347,8 @@ public void untested_spec304_requestShouldNotPerformHeavyComputations() throws E } @Override @Test - public void untested_spec305_cancelMustNotSynchronouslyPerformHeavyCompuatation() throws Exception { - publisherVerification.untested_spec305_cancelMustNotSynchronouslyPerformHeavyCompuatation(); + public void untested_spec305_cancelMustNotSynchronouslyPerformHeavyComputation() throws Exception { + publisherVerification.untested_spec305_cancelMustNotSynchronouslyPerformHeavyComputation(); } @Override @Test @@ -340,6 +370,11 @@ public void required_spec309_requestZeroMustSignalIllegalArgumentException() thr public void required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() throws Throwable { publisherVerification.required_spec309_requestNegativeNumberMustSignalIllegalArgumentException(); } + + @Override @Test + public void optional_spec309_requestNegativeNumberMaySignalIllegalArgumentExceptionWithSpecificMessage() throws Throwable { + publisherVerification.optional_spec309_requestNegativeNumberMaySignalIllegalArgumentExceptionWithSpecificMessage(); + } @Override @Test public void required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() throws Throwable { @@ -366,9 +401,9 @@ public void required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue() publisherVerification.required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue(); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.4 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#1.4 // for multiple subscribers - @Test + @Test public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError() throws Throwable { optionalMultipleSubscribersTest(2, new Function() { @Override @@ -394,14 +429,14 @@ public TestSetup apply(Long aLong) throws Throwable { sub1.expectError(ex); sub2.expectError(ex); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); }}; } }); } ////////////////////// SUBSCRIBER RULES VERIFICATION /////////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#4.1 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#4.1 // A Processor // must obey all Subscriber rules on its consuming side @@ -466,12 +501,12 @@ public void mustImmediatelyPassOnOnErrorEventsReceivedFromItsUpstreamToItsDownst sendError(ex); sub.expectError(ex); // "immediately", i.e. without a preceding request - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); }}; } /////////////////////// DELEGATED TESTS, A PROCESSOR "IS A" SUBSCRIBER ////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#4.1 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#4.1 @Test public void required_exerciseWhiteboxHappyPath() throws Throwable { @@ -504,7 +539,7 @@ public void untested_spec204_mustConsiderTheSubscriptionAsCancelledInAfterReciev } @Override @Test - public void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception { + public void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Throwable { subscriberVerification.required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal(); } @@ -558,6 +593,19 @@ public void untested_spec213_failingOnSignalInvocation() throws Exception { subscriberVerification.untested_spec213_failingOnSignalInvocation(); } + @Override @Test + public void required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberVerification.required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + @Override @Test + public void required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberVerification.required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + @Override @Test + public void required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberVerification.required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + @Override @Test public void untested_spec301_mustNotBeCalledOutsideSubscriberContext() throws Exception { subscriberVerification.untested_spec301_mustNotBeCalledOutsideSubscriberContext(); @@ -641,7 +689,7 @@ public TestSetup apply(Long subscribers) throws Throwable { sub1.expectCompletion(env.defaultTimeoutMillis()); sub2.expectCompletion(env.defaultTimeoutMillis()); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); }}; } }); @@ -732,4 +780,4 @@ public void expectError(Throwable cause, long timeoutMillis) throws InterruptedE } } } -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/reactivestreams/tck/PublisherVerification.java b/tck/src/main/java/org/reactivestreams/tck/PublisherVerification.java index a2d5eaa2..54badd61 100644 --- a/tck/src/main/java/org/reactivestreams/tck/PublisherVerification.java +++ b/tck/src/main/java/org/reactivestreams/tck/PublisherVerification.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; @@ -7,13 +18,14 @@ import org.reactivestreams.tck.TestEnvironment.Latch; import org.reactivestreams.tck.TestEnvironment.ManualSubscriber; import org.reactivestreams.tck.TestEnvironment.ManualSubscriberWithSubscriptionSupport; -import org.reactivestreams.tck.support.Function; -import org.reactivestreams.tck.support.Optional; -import org.reactivestreams.tck.support.PublisherVerificationRules; +import org.reactivestreams.tck.flow.support.Function; +import org.reactivestreams.tck.flow.support.Optional; +import org.reactivestreams.tck.flow.support.PublisherVerificationRules; import org.testng.SkipException; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import java.lang.Override; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; @@ -21,6 +33,7 @@ import java.util.Collections; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import static org.testng.Assert.assertEquals; @@ -37,6 +50,11 @@ public abstract class PublisherVerification implements PublisherVerificationR private static final long DEFAULT_PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS = 300L; private final TestEnvironment env; + + /** + * The amount of time after which a cancelled Subscriber reference should be dropped. + * See Rule 3.13 for details. + */ private final long publisherReferenceGCTimeoutMillis; /** @@ -72,7 +90,7 @@ public static long envPublisherReferenceGCTimeoutMillis() { if (envMillis == null) return DEFAULT_PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS; else try { return Long.parseLong(envMillis); - } catch(NumberFormatException ex) { + } catch (NumberFormatException ex) { throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", PUBLISHER_REFERENCE_GC_TIMEOUT_MILLIS_ENV, envMillis), ex); } } @@ -85,10 +103,14 @@ public static long envPublisherReferenceGCTimeoutMillis() { public abstract Publisher createPublisher(long elements); /** - * Return a Publisher in {@code error} state in order to run additional tests on it, - * or {@code null} in order to skip them. + * By implementing this method, additional TCK tests concerning a "failed" publishers will be run. + * + * The expected behaviour of the {@link Publisher} returned by this method is hand out a subscription, + * followed by signalling {@code onError} on it, as specified by Rule 1.9. + * + * If you ignore these additional tests, return {@code null} from this method. */ - public abstract Publisher createErrorStatePublisher(); + public abstract Publisher createFailedPublisher(); /** @@ -106,7 +128,9 @@ public long maxElementsFromPublisher() { /** * Override and return {@code true} in order to skip executing tests marked as {@code Stochastic}. - * Such tests MAY sometimes fail even though the impl + * Stochastic in this case means that the Rule is impossible or infeasible to deterministically verify— + * usually this means that this test case can yield false positives ("be green") even if for some case, + * the given implementation may violate the tested behaviour. */ public boolean skipStochasticTests() { return false; @@ -117,21 +141,13 @@ public boolean skipStochasticTests() { * {@code Subscription} actually solves the "unbounded recursion" problem by not allowing the number of * recursive calls to exceed the number returned by this method. * - * @see reactive streams spec, rule 3.3 + * @see reactive streams spec, rule 3.3 * @see PublisherVerification#required_spec303_mustNotAllowUnboundedRecursion() */ public long boundedDepthOfOnNextAndRequestRecursion() { return 1; } - /** - * The amount of time after which a cancelled Subscriber reference should be dropped. - * See Rule 3.13 for details. - */ - final public long publisherReferenceGCTimeoutMillis() { - return publisherReferenceGCTimeoutMillis; - } - ////////////////////// TEST ENV CLEANUP ///////////////////////////////////// @BeforeMethod @@ -190,7 +206,6 @@ public void required_validate_boundedDepthOfOnNextAndRequestRecursion() throws E ////////////////////// SPEC RULE VERIFICATION /////////////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.1 @Override @Test public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable { activePublisherTest(5, false, new PublisherTestRun() { @@ -198,22 +213,24 @@ public void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfPr public void run(Publisher pub) throws InterruptedException { ManualSubscriber sub = env.newManualSubscriber(pub); - - sub.expectNone(String.format("Publisher %s produced value before the first `request`: ", pub)); - sub.request(1); - sub.nextElement(String.format("Publisher %s produced no element after first `request`", pub)); - sub.expectNone(String.format("Publisher %s produced unrequested: ", pub)); - - sub.request(1); - sub.request(2); - sub.nextElements(3, env.defaultTimeoutMillis(), String.format("Publisher %s produced less than 3 elements after two respective `request` calls", pub)); - - sub.expectNone(String.format("Publisher %sproduced unrequested ", pub)); + try { + sub.expectNone(String.format("Publisher %s produced value before the first `request`: ", pub)); + sub.request(1); + sub.nextElement(String.format("Publisher %s produced no element after first `request`", pub)); + sub.expectNone(String.format("Publisher %s produced unrequested: ", pub)); + + sub.request(1); + sub.request(2); + sub.nextElements(3, env.defaultTimeoutMillis(), String.format("Publisher %s produced less than 3 elements after two respective `request` calls", pub)); + + sub.expectNone(String.format("Publisher %sproduced unrequested ", pub)); + } finally { + sub.cancel(); + } } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.2 @Override @Test public void required_spec102_maySignalLessThanRequestedAndTerminateSubscription() throws Throwable { final int elements = 3; @@ -230,7 +247,6 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.3 @Override @Test public void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable { final int iterations = 100; @@ -244,9 +260,9 @@ public Void apply(final Integer runNumber) throws Throwable { public void run(Publisher pub) throws Throwable { final Latch completionLatch = new Latch(env); + final AtomicInteger gotElements = new AtomicInteger(0); pub.subscribe(new Subscriber() { private Subscription subs; - private long gotElements = 0; private ConcurrentAccessBarrier concurrentAccessBarrier = new ConcurrentAccessBarrier(); @@ -300,8 +316,7 @@ public void onNext(T ignore) { final String signal = String.format("onNext(%s)", ignore); concurrentAccessBarrier.enterSignal(signal); - gotElements += 1; - if (gotElements <= elements) // requesting one more than we know are in the stream (some Publishers need this) + if (gotElements.incrementAndGet() <= elements) // requesting one more than we know are in the stream (some Publishers need this) subs.request(1); concurrentAccessBarrier.leaveSignal(signal); @@ -329,7 +344,10 @@ public void onComplete() { } }); - completionLatch.expectClose(elements * env.defaultTimeoutMillis(), "Expected 10 elements to be drained"); + completionLatch.expectClose( + elements * env.defaultTimeoutMillis(), + String.format("Failed in iteration %d of %d. Expected completion signal after signalling %d elements (signalled %d), yet did not receive it", + runNumber, iterations, elements, gotElements.get())); } }); return null; @@ -337,25 +355,32 @@ public void onComplete() { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.4 @Override @Test public void optional_spec104_mustSignalOnErrorWhenFails() throws Throwable { try { whenHasErrorPublisherTest(new PublisherTestRun() { @Override public void run(final Publisher pub) throws InterruptedException { - final Latch latch = new Latch(env); + final Latch onErrorlatch = new Latch(env); + final Latch onSubscribeLatch = new Latch(env); pub.subscribe(new TestEnvironment.TestSubscriber(env) { + @Override + public void onSubscribe(Subscription subs) { + onSubscribeLatch.assertOpen("Only one onSubscribe call expected"); + onSubscribeLatch.close(); + } @Override public void onError(Throwable cause) { - latch.assertOpen(String.format("Error-state Publisher %s called `onError` twice on new Subscriber", pub)); - latch.close(); + onSubscribeLatch.assertClosed("onSubscribe should be called prior to onError always"); + onErrorlatch.assertOpen(String.format("Error-state Publisher %s called `onError` twice on new Subscriber", pub)); + onErrorlatch.close(); } }); - latch.expectClose(String.format("Error-state Publisher %s did not call `onError` on new Subscriber", pub)); + onSubscribeLatch.expectClose("Should have received onSubscribe"); + onErrorlatch.expectClose(String.format("Error-state Publisher %s did not call `onError` on new Subscriber", pub)); - env.verifyNoAsyncErrors(env.defaultTimeoutMillis()); + env.verifyNoAsyncErrors(); } }); } catch (SkipException se) { @@ -367,7 +392,6 @@ public void onError(Throwable cause) { } } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.5 @Override @Test public void required_spec105_mustSignalOnCompleteWhenFiniteStreamTerminates() throws Throwable { activePublisherTest(3, true, new PublisherTestRun() { @@ -383,7 +407,6 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.5 @Override @Test public void optional_spec105_emptyStreamMustTerminateBySignallingOnComplete() throws Throwable { optionalActivePublisherTest(0, true, new PublisherTestRun() { @@ -397,13 +420,11 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.6 @Override @Test public void untested_spec106_mustConsiderSubscriptionCancelledAfterOnErrorOrOnCompleteHasBeenCalled() throws Throwable { notVerified(); // not really testable without more control over the Publisher } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.7 @Override @Test public void required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled() throws Throwable { activePublisherTest(1, true, new PublisherTestRun() { @@ -420,74 +441,166 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.7 @Override @Test public void untested_spec107_mustNotEmitFurtherSignalsOnceOnErrorHasBeenSignalled() throws Throwable { notVerified(); // can we meaningfully test this, without more control over the publisher? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.8 @Override @Test public void untested_spec108_possiblyCanceledSubscriptionShouldNotReceiveOnErrorOrOnCompleteSignals() throws Throwable { notVerified(); // can we meaningfully test this? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.9 @Override @Test public void untested_spec109_subscribeShouldNotThrowNonFatalThrowable() throws Throwable { - notVerified(); // cannot be meaningfully tested, or can it? + notVerified(); // can we meaningfully test this? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.10 @Override @Test - public void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice() throws Throwable { - notVerified(); // can we meaningfully test this? + public void required_spec109_subscribeThrowNPEOnNullSubscriber() throws Throwable { + activePublisherTest(0, false, new PublisherTestRun() { + @Override + public void run(Publisher pub) throws Throwable { + try { + pub.subscribe(null); + env.flop("Publisher did not throw a NullPointerException when given a null Subscribe in subscribe"); + } catch (NullPointerException ignored) { + // valid behaviour + } + env.verifyNoAsyncErrorsNoDelay(); + } + }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.11 @Override @Test - public void optional_spec111_maySupportMultiSubscribe() throws Throwable { - optionalActivePublisherTest(1, false, new PublisherTestRun() { + public void required_spec109_mustIssueOnSubscribeForNonNullSubscriber() throws Throwable { + activePublisherTest(0, false, new PublisherTestRun() { @Override public void run(Publisher pub) throws Throwable { - ManualSubscriber sub1 = env.newManualSubscriber(pub); - ManualSubscriber sub2 = env.newManualSubscriber(pub); - - env.verifyNoAsyncErrors(); + final Latch onSubscribeLatch = new Latch(env); + final AtomicReference cancel = new AtomicReference(); + try { + pub.subscribe(new Subscriber() { + @Override + public void onError(Throwable cause) { + onSubscribeLatch.assertClosed("onSubscribe should be called prior to onError always"); + } + + @Override + public void onSubscribe(Subscription subs) { + cancel.set(subs); + onSubscribeLatch.assertOpen("Only one onSubscribe call expected"); + onSubscribeLatch.close(); + } + + @Override + public void onNext(T elem) { + onSubscribeLatch.assertClosed("onSubscribe should be called prior to onNext always"); + } + + @Override + public void onComplete() { + onSubscribeLatch.assertClosed("onSubscribe should be called prior to onComplete always"); + } + }); + onSubscribeLatch.expectClose("Should have received onSubscribe"); + env.verifyNoAsyncErrorsNoDelay(); + } finally { + Subscription s = cancel.getAndSet(null); + if (s != null) { + s.cancel(); + } + } } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.12 @Override @Test - public void required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe() throws Throwable { + public void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe() throws Throwable { whenHasErrorPublisherTest(new PublisherTestRun() { @Override public void run(Publisher pub) throws Throwable { final Latch onErrorLatch = new Latch(env); + final Latch onSubscribeLatch = new Latch(env); ManualSubscriberWithSubscriptionSupport sub = new ManualSubscriberWithSubscriptionSupport(env) { @Override public void onError(Throwable cause) { + onSubscribeLatch.assertClosed("onSubscribe should be called prior to onError always"); onErrorLatch.assertOpen("Only one onError call expected"); onErrorLatch.close(); } @Override public void onSubscribe(Subscription subs) { - env.flop("onSubscribe should not be called if Publisher is unable to subscribe a Subscriber"); + onSubscribeLatch.assertOpen("Only one onSubscribe call expected"); + onSubscribeLatch.close(); } }; pub.subscribe(sub); - onErrorLatch.assertClosed("Should have received onError"); + onSubscribeLatch.expectClose("Should have received onSubscribe"); + onErrorLatch.expectClose("Should have received onError"); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + @Override @Test + public void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice() throws Throwable { + notVerified(); // can we meaningfully test this? + } + + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#1.11 + @Override @Test + public void optional_spec111_maySupportMultiSubscribe() throws Throwable { + optionalActivePublisherTest(1, false, new PublisherTestRun() { + @Override + public void run(Publisher pub) throws Throwable { + ManualSubscriber sub1 = env.newManualSubscriber(pub); + ManualSubscriber sub2 = env.newManualSubscriber(pub); + + try { + env.verifyNoAsyncErrors(); + } finally { + try { + sub1.cancel(); + } finally { + sub2.cancel(); + } + } + } + }); + } + + @Override @Test + public void optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals() throws Throwable { + optionalActivePublisherTest(1, false, new PublisherTestRun() { + @Override + public void run(Publisher pub) throws Throwable { + ManualSubscriber sub1 = env.newManualSubscriber(pub); + ManualSubscriber sub2 = env.newManualSubscriber(pub); + // Since we're testing the case when the Publisher DOES support the optional multi-subscribers scenario, + // and decides if it handles them uni-cast or multi-cast, we don't know which subscriber will receive an + // onNext (and optional onComplete) signal(s) and which just onComplete signal. + // Plus, even if subscription assumed to be unicast, it's implementation choice, which one will be signalled + // with onNext. + sub1.requestNextElementOrEndOfStream(); + sub2.requestNextElementOrEndOfStream(); + try { + env.verifyNoAsyncErrors(); + } finally { + try { + sub1.cancel(); + } finally { + sub2.cancel(); + } } + } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.13 @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable { + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable { optionalActivePublisherTest(5, true, new PublisherTestRun() { // This test is skipped if the publisher is unbounded (never sends onComplete) @Override public void run(Publisher pub) throws InterruptedException { @@ -536,9 +649,8 @@ public void run(Publisher pub) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.13 @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable { + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable { optionalActivePublisherTest(3, false, new PublisherTestRun() { // This test is skipped if the publisher cannot produce enough elements @Override public void run(Publisher pub) throws Throwable { @@ -569,9 +681,8 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#1.13 @Override @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable { + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable { optionalActivePublisherTest(3, true, new PublisherTestRun() { // This test is skipped if the publisher is unbounded (never sends onComplete) @Override public void run(Publisher pub) throws Throwable { @@ -605,7 +716,6 @@ public void run(Publisher pub) throws Throwable { ///////////////////// SUBSCRIPTION TESTS ////////////////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.2 @Override @Test public void required_spec302_mustAllowSynchronousRequestCallsFromOnNextAndOnSubscribe() throws Throwable { activePublisherTest(6, false, new PublisherTestRun() { @@ -630,13 +740,11 @@ public void onNext(T element) { env.subscribe(pub, sub); - long delay = env.defaultTimeoutMillis(); - env.verifyNoAsyncErrors(delay); + env.verifyNoAsyncErrors(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.3 @Override @Test public void required_spec303_mustNotAllowUnboundedRecursion() throws Throwable { final long oneMoreThanBoundedLimit = boundedDepthOfOnNextAndRequestRecursion() + 1; @@ -709,7 +817,7 @@ public void onError(Throwable cause) { "awaited at-most %s signals (`maxOnNextSignalsInRecursionTest()`) or completion", oneMoreThanBoundedLimit); runCompleted.expectClose(env.defaultTimeoutMillis(), msg); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } finally { // since the request/onNext recursive calls may keep the publisher running "forever", // we MUST cancel it manually before exiting this test case @@ -719,19 +827,16 @@ public void onError(Throwable cause) { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.4 @Override @Test public void untested_spec304_requestShouldNotPerformHeavyComputations() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.5 @Override @Test - public void untested_spec305_cancelMustNotSynchronouslyPerformHeavyCompuatation() throws Exception { + public void untested_spec305_cancelMustNotSynchronouslyPerformHeavyComputation() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.6 @Override @Test public void required_spec306_afterSubscriptionIsCancelledRequestMustBeNops() throws Throwable { activePublisherTest(3, false, new PublisherTestRun() { @@ -760,12 +865,11 @@ public void cancel() { sub.request(1); sub.expectNone(); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.7 @Override @Test public void required_spec307_afterSubscriptionIsCancelledAdditionalCancelationsMustBeNops() throws Throwable { activePublisherTest(1, false, new PublisherTestRun() { @@ -781,24 +885,22 @@ public void run(Publisher pub) throws Throwable { subs.cancel(); sub.expectNone(); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.9 @Override @Test public void required_spec309_requestZeroMustSignalIllegalArgumentException() throws Throwable { activePublisherTest(10, false, new PublisherTestRun() { @Override public void run(Publisher pub) throws Throwable { final ManualSubscriber sub = env.newManualSubscriber(pub); sub.request(0); - sub.expectErrorWithMessage(IllegalArgumentException.class, "3.9"); // we do require implementations to mention the rule number at the very least + sub.expectError(IllegalArgumentException.class); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.9 @Override @Test public void required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() throws Throwable { activePublisherTest(10, false, new PublisherTestRun() { @@ -806,13 +908,27 @@ public void required_spec309_requestNegativeNumberMustSignalIllegalArgumentExcep public void run(Publisher pub) throws Throwable { final ManualSubscriber sub = env.newManualSubscriber(pub); final Random r = new Random(); - sub.request(-r.nextInt(Integer.MAX_VALUE)); - sub.expectErrorWithMessage(IllegalArgumentException.class, "3.9"); // we do require implementations to mention the rule number at the very least + sub.request(-r.nextInt(Integer.MAX_VALUE) - 1); + // we do require implementations to mention the rule number at the very least, or mentioning that the non-negative request is the problem + sub.expectError(IllegalArgumentException.class); + } + }); + } + + @Override @Test + public void optional_spec309_requestNegativeNumberMaySignalIllegalArgumentExceptionWithSpecificMessage() throws Throwable { + optionalActivePublisherTest(10, false, new PublisherTestRun() { + @Override + public void run(Publisher pub) throws Throwable { + final ManualSubscriber sub = env.newManualSubscriber(pub); + final Random r = new Random(); + sub.request(-r.nextInt(Integer.MAX_VALUE) - 1); + // we do require implementations to mention the rule number at the very least, or mentioning that the non-negative request is the problem + sub.expectErrorWithMessage(IllegalArgumentException.class, Arrays.asList("3.9", "non-positive subscription request", "negative subscription request")); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.12 @Override @Test public void required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() throws Throwable { // the publisher is able to signal more elements than the subscriber will be requesting in total @@ -872,10 +988,9 @@ > AsyncPublisher receives cancel() - handles it right away, by "stopping itself" } }); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.13 @Override @Test public void required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber() throws Throwable { final ReferenceQueue> queue = new ReferenceQueue>(); @@ -908,12 +1023,11 @@ public void run(Publisher pub) throws Throwable { env.flop(String.format("Publisher %s did not drop reference to test subscriber after subscription cancellation", pub)); } - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.17 @Override @Test public void required_spec317_mustSupportAPendingElementCountUpToLongMaxValue() throws Throwable { final int totalElements = 3; @@ -927,12 +1041,11 @@ public void run(Publisher pub) throws Throwable { sub.nextElements(totalElements); sub.expectCompletion(); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.17 @Override @Test public void required_spec317_mustSupportACumulativePendingElementCountUpToLongMaxValue() throws Throwable { final int totalElements = 3; @@ -949,7 +1062,7 @@ public void run(Publisher pub) throws Throwable { sub.expectCompletion(); try { - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } finally { sub.cancel(); } @@ -958,7 +1071,6 @@ public void run(Publisher pub) throws Throwable { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.17 @Override @Test public void required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue() throws Throwable { activePublisherTest(Integer.MAX_VALUE, false, new PublisherTestRun() { @@ -975,6 +1087,8 @@ public void onNext(T element) { if (callsCounter > 0) { subscription.value().request(Long.MAX_VALUE - 1); callsCounter--; + } else { + subscription.value().cancel(); } } else { env.flop(String.format("Subscriber::onNext(%s) called before Subscriber::onSubscribe", element)); @@ -989,7 +1103,7 @@ public void onNext(T element) { // no onError should be signalled try { - env.verifyNoAsyncErrors(env.defaultTimeoutMillis()); + env.verifyNoAsyncErrors(); } finally { sub.cancel(); } @@ -1022,7 +1136,7 @@ public void activePublisherTest(long elements, boolean completionSignalRequired, } else { Publisher pub = createPublisher(elements); body.run(pub); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } } @@ -1057,7 +1171,7 @@ public void optionalActivePublisherTest(long elements, boolean completionSignalR public static final String SKIPPING_NO_ERROR_PUBLISHER_AVAILABLE = "Skipping because no error state Publisher provided, and the test requires it. " + - "Please implement PublisherVerification#createErrorStatePublisher to run this test."; + "Please implement PublisherVerification#createFailedPublisher to run this test."; public static final String SKIPPING_OPTIONAL_TEST_FAILED = "Skipping, because provided Publisher does not pass this *additional* verification."; @@ -1065,7 +1179,7 @@ public void optionalActivePublisherTest(long elements, boolean completionSignalR * Additional test for Publisher in error state */ public void whenHasErrorPublisherTest(PublisherTestRun body) throws Throwable { - potentiallyPendingTest(createErrorStatePublisher(), body, SKIPPING_NO_ERROR_PUBLISHER_AVAILABLE); + potentiallyPendingTest(createFailedPublisher(), body, SKIPPING_NO_ERROR_PUBLISHER_AVAILABLE); } public void potentiallyPendingTest(Publisher pub, PublisherTestRun body) throws Throwable { diff --git a/tck/src/main/java/org/reactivestreams/tck/SubscriberBlackboxVerification.java b/tck/src/main/java/org/reactivestreams/tck/SubscriberBlackboxVerification.java index 8a43a625..8931ec93 100644 --- a/tck/src/main/java/org/reactivestreams/tck/SubscriberBlackboxVerification.java +++ b/tck/src/main/java/org/reactivestreams/tck/SubscriberBlackboxVerification.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; @@ -5,9 +16,9 @@ import org.reactivestreams.Subscription; import org.reactivestreams.tck.TestEnvironment.ManualPublisher; import org.reactivestreams.tck.TestEnvironment.ManualSubscriber; -import org.reactivestreams.tck.support.Optional; -import org.reactivestreams.tck.support.SubscriberBlackboxVerificationRules; -import org.reactivestreams.tck.support.TestException; +import org.reactivestreams.tck.flow.support.Optional; +import org.reactivestreams.tck.flow.support.SubscriberBlackboxVerificationRules; +import org.reactivestreams.tck.flow.support.TestException; import org.testng.SkipException; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -18,6 +29,7 @@ import java.util.concurrent.Executors; import static org.reactivestreams.tck.SubscriberWhiteboxVerification.BlackboxSubscriberProxy; +import static org.testng.Assert.assertTrue; /** * Provides tests for verifying {@link org.reactivestreams.Subscriber} and {@link org.reactivestreams.Subscription} @@ -30,7 +42,7 @@ * @see org.reactivestreams.Subscriber * @see org.reactivestreams.Subscription */ -public abstract class SubscriberBlackboxVerification extends WithHelperPublisher +public abstract class SubscriberBlackboxVerification extends WithHelperPublisher implements SubscriberBlackboxVerificationRules { protected final TestEnvironment env; @@ -47,6 +59,16 @@ protected SubscriberBlackboxVerification(TestEnvironment env) { */ public abstract Subscriber createSubscriber(); + /** + * Override this method if the Subscriber implementation you are verifying + * needs an external signal before it signals demand to its Publisher. + * + * By default this method does nothing. + */ + public void triggerRequest(final Subscriber subscriber) { + // this method is intentionally left blank + } + // ENV SETUP /** @@ -67,28 +89,40 @@ public void setUp() throws Exception { ////////////////////// SPEC RULE VERIFICATION /////////////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.1 @Override @Test public void required_spec201_blackbox_mustSignalDemandViaSubscriptionRequest() throws Throwable { blackboxSubscriberTest(new BlackboxTestStageTestRun() { @Override public void run(BlackboxTestStage stage) throws InterruptedException { - final long n = stage.expectRequest();// assuming subscriber wants to consume elements... + triggerRequest(stage.subProxy().sub()); + final long requested = stage.expectRequest();// assuming subscriber wants to consume elements... + final long signalsToEmit = Math.min(requested, 512); // protecting against Subscriber which sends ridiculous large demand // should cope with up to requested number of elements - for (int i = 0; i < n; i++) + for (int i = 0; i < signalsToEmit && sampleIsCancelled(stage, i, 10); i++) stage.signalNext(); + + // we complete after `signalsToEmit` (which can be less than `requested`), + // which is legal under https://github.com/reactive-streams/reactive-streams-jvm#1.2 + stage.sendCompletion(); + } + + /** + * In order to allow some "skid" and not check state on each iteration, + * only check {@code stage.isCancelled} every {@code checkInterval}'th iteration. + */ + private boolean sampleIsCancelled(BlackboxTestStage stage, int i, int checkInterval) throws InterruptedException { + if (i % checkInterval == 0) return stage.isCancelled(); + else return false; } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.2 @Override @Test public void untested_spec202_blackbox_shouldAsynchronouslyDispatch() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.3 @Override @Test public void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable { blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { @@ -120,12 +154,11 @@ public void cancel() { sub.onSubscribe(subs); sub.onComplete(); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.3 @Override @Test public void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable { blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { @@ -159,18 +192,16 @@ public void cancel() { sub.onSubscribe(subs); sub.onError(new TestException()); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.4 @Override @Test public void untested_spec204_blackbox_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.5 @Override @Test public void required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception { new BlackboxTestStage(env) {{ @@ -195,160 +226,201 @@ public String toString() { }); secondSubscriptionCancelled.expectClose("Expected SecondSubscription given to subscriber to be cancelled, but `Subscription.cancel()` was not called."); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); + sendCompletion(); // we're done, complete the subscriber under test }}; } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.6 @Override @Test public void untested_spec206_blackbox_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.7 @Override @Test public void untested_spec207_blackbox_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? // the same thread part of the clause can be verified but that is not very useful, or is it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.8 @Override @Test public void untested_spec208_blackbox_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable { notVerified(); // cannot be meaningfully tested as black box, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.9 @Override @Test public void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable { - blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { - @Override + blackboxSubscriberTest(new BlackboxTestStageTestRun() { + @Override @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void run(BlackboxTestStage stage) throws Throwable { - final Publisher pub = new Publisher() { - @Override public void subscribe(final Subscriber s) { - s.onSubscribe(new Subscription() { - private boolean completed = false; - - @Override public void request(long n) { - if (!completed) { - completed = true; - s.onComplete(); // Publisher now realises that it is in fact already completed - } - } - - @Override public void cancel() { - // noop, ignore - } - }); - } - }; - - final Subscriber sub = createSubscriber(); - final BlackboxSubscriberProxy probe = stage.createBlackboxSubscriberProxy(env, sub); - - pub.subscribe(probe); - probe.expectCompletion(); - probe.expectNone(); - - env.verifyNoAsyncErrors(); + triggerRequest(stage.subProxy().sub()); + final long notUsed = stage.expectRequest(); // received request signal + stage.sub().onComplete(); + stage.subProxy().expectCompletion(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.9 @Override @Test public void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable { - blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { - @Override + blackboxSubscriberTest(new BlackboxTestStageTestRun() { + @Override @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void run(BlackboxTestStage stage) throws Throwable { - final Publisher pub = new Publisher() { - @Override - public void subscribe(Subscriber s) { - s.onComplete(); - } - }; - - final Subscriber sub = createSubscriber(); - final BlackboxSubscriberProxy probe = stage.createBlackboxSubscriberProxy(env, sub); - - pub.subscribe(probe); - probe.expectCompletion(); - - env.verifyNoAsyncErrors(); + final Subscriber sub = stage.sub(); + sub.onComplete(); + stage.subProxy().expectCompletion(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.10 @Override @Test public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable { blackboxSubscriberTest(new BlackboxTestStageTestRun() { - @Override - @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Override @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + public void run(BlackboxTestStage stage) throws Throwable { + triggerRequest(stage.subProxy().sub()); + final long notUsed = stage.expectRequest(); // received request signal + stage.sub().onError(new TestException()); // in response to that, we fail + stage.subProxy().expectError(Throwable.class); + } + }); + } + + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.10 + @Override @Test + public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall() throws Throwable { + blackboxSubscriberTest(new BlackboxTestStageTestRun() { + @Override @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void run(BlackboxTestStage stage) throws Throwable { + stage.sub().onError(new TestException()); stage.subProxy().expectError(Throwable.class); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.11 @Override @Test public void untested_spec211_blackbox_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.12 @Override @Test public void untested_spec212_blackbox_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality() throws Throwable { notVerified(); // cannot be meaningfully tested as black box, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.13 @Override @Test public void untested_spec213_blackbox_failingOnSignalInvocation() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } + @Override @Test + public void required_spec213_blackbox_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { + @Override + public void run(BlackboxTestStage stage) throws Throwable { + + { + final Subscriber sub = createSubscriber(); + boolean gotNPE = false; + try { + sub.onSubscribe(null); + } catch(final NullPointerException expected) { + gotNPE = true; + } + assertTrue(gotNPE, "onSubscribe(null) did not throw NullPointerException"); + } + + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + @Override @Test + public void required_spec213_blackbox_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { + @Override + public void run(BlackboxTestStage stage) throws Throwable { + final Subscription subscription = new Subscription() { + @Override public void request(final long elements) {} + @Override public void cancel() {} + }; + + { + final Subscriber sub = createSubscriber(); + boolean gotNPE = false; + sub.onSubscribe(subscription); + try { + sub.onNext(null); + } catch(final NullPointerException expected) { + gotNPE = true; + } + assertTrue(gotNPE, "onNext(null) did not throw NullPointerException"); + } + + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + @Override @Test + public void required_spec213_blackbox_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + blackboxSubscriberWithoutSetupTest(new BlackboxTestStageTestRun() { + @Override + public void run(BlackboxTestStage stage) throws Throwable { + final Subscription subscription = new Subscription() { + @Override public void request(final long elements) {} + @Override public void cancel() {} + }; + + { + final Subscriber sub = createSubscriber(); + boolean gotNPE = false; + sub.onSubscribe(subscription); + try { + sub.onError(null); + } catch(final NullPointerException expected) { + gotNPE = true; + } + assertTrue(gotNPE, "onError(null) did not throw NullPointerException"); + } + + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + ////////////////////// SUBSCRIPTION SPEC RULE VERIFICATION ////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.1 @Override @Test public void untested_spec301_blackbox_mustNotBeCalledOutsideSubscriberContext() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.8 @Override @Test - public void required_spec308_blackbox_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable { + public void untested_spec308_blackbox_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable { notVerified(); // cannot be meaningfully tested as black box, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.10 @Override @Test public void untested_spec310_blackbox_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.11 @Override @Test public void untested_spec311_blackbox_requestMaySynchronouslyCallOnCompleteOrOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.14 @Override @Test public void untested_spec314_blackbox_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.15 @Override @Test public void untested_spec315_blackbox_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.16 @Override @Test public void untested_spec316_blackbox_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? @@ -429,4 +501,4 @@ public T nextT() throws InterruptedException { public void notVerified() { throw new SkipException("Not verified using this TCK."); } -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/reactivestreams/tck/SubscriberWhiteboxVerification.java b/tck/src/main/java/org/reactivestreams/tck/SubscriberWhiteboxVerification.java index 3fcef13c..a22354a4 100644 --- a/tck/src/main/java/org/reactivestreams/tck/SubscriberWhiteboxVerification.java +++ b/tck/src/main/java/org/reactivestreams/tck/SubscriberWhiteboxVerification.java @@ -1,13 +1,23 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.reactivestreams.tck.TestEnvironment.*; -import org.reactivestreams.tck.support.Function; -import org.reactivestreams.tck.support.Optional; -import org.reactivestreams.tck.support.TestException; -import org.reactivestreams.tck.support.SubscriberWhiteboxVerificationRules; +import org.reactivestreams.tck.flow.support.Optional; +import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; +import org.reactivestreams.tck.flow.support.TestException; import org.testng.SkipException; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -17,11 +27,11 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; /** - * Provides tests for verifying {@link org.reactivestreams.Subscriber} and {@link org.reactivestreams.Subscription} specification rules. + * Provides whitebox style tests for verifying {@link org.reactivestreams.Subscriber} + * and {@link org.reactivestreams.Subscription} specification rules. * * @see org.reactivestreams.Subscriber * @see org.reactivestreams.Subscription @@ -97,7 +107,7 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { ////////////////////// SPEC RULE VERIFICATION /////////////////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.1 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.1 @Override @Test public void required_spec201_mustSignalDemandViaSubscriptionRequest() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -111,13 +121,13 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.2 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.2 @Override @Test public void untested_spec202_shouldAsynchronouslyDispatch() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.3 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.3 @Override @Test public void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable { subscriberTestWithoutSetup(new TestStageTestRun() { @@ -151,12 +161,12 @@ public void cancel() { sub.onSubscribe(subs); sub.onComplete(); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.3 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.3 @Override @Test public void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable { subscriberTestWithoutSetup(new TestStageTestRun() { @@ -192,60 +202,64 @@ public void cancel() { sub.onSubscribe(subs); sub.onError(new TestException()); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.4 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.4 @Override @Test public void untested_spec204_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.5 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.5 @Override @Test - public void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception { - new WhiteboxTestStage(env) {{ - // try to subscribe another time, if the subscriber calls `probe.registerOnSubscribe` the test will fail - final Latch secondSubscriptionCancelled = new Latch(env); - sub().onSubscribe( - new Subscription() { - @Override - public void request(long elements) { - env.flop(String.format("Subscriber %s illegally called `subscription.request(%s)`", sub(), elements)); - } + public void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Throwable { + subscriberTest(new TestStageTestRun() { + @Override + public void run(WhiteboxTestStage stage) throws Throwable { + // try to subscribe another time, if the subscriber calls `probe.registerOnSubscribe` the test will fail + final Latch secondSubscriptionCancelled = new Latch(env); + final Subscriber sub = stage.sub(); + final Subscription subscription = new Subscription() { + @Override + public void request(long elements) { + // ignore... + } - @Override - public void cancel() { - secondSubscriptionCancelled.close(); - } + @Override + public void cancel() { + secondSubscriptionCancelled.close(); + } - @Override - public String toString() { - return "SecondSubscription(should get cancelled)"; - } - }); + @Override + public String toString() { + return "SecondSubscription(should get cancelled)"; + } + }; + sub.onSubscribe(subscription); - secondSubscriptionCancelled.expectClose("Expected 2nd Subscription given to subscriber to be cancelled, but `Subscription.cancel()` was not called."); - env.verifyNoAsyncErrors(); - }}; + secondSubscriptionCancelled.expectClose("Expected 2nd Subscription given to subscriber to be cancelled, but `Subscription.cancel()` was not called"); + env.verifyNoAsyncErrors(); + } + }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.6 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.6 @Override @Test public void untested_spec206_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.7 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.7 @Override @Test public void untested_spec207_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? // the same thread part of the clause can be verified but that is not very useful, or is it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.8 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.8 @Override @Test public void required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -263,7 +277,7 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.9 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.9 @Override @Test public void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -278,7 +292,7 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.9 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.9 @Override @Test public void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -292,7 +306,7 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.10 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.10 @Override @Test public void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -305,12 +319,12 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { stage.sendError(ex); stage.probe.expectError(ex); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.10 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.10 @Override @Test public void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -320,38 +334,103 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { stage.sendError(ex); stage.probe.expectError(ex); - env.verifyNoAsyncErrors(); + env.verifyNoAsyncErrorsNoDelay(); } }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.11 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.11 @Override @Test public void untested_spec211_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.12 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.12 @Override @Test public void untested_spec212_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality_specViolation() throws Throwable { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#2.13 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.13 @Override @Test public void untested_spec213_failingOnSignalInvocation() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.13 + @Override @Test + public void required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberTest(new TestStageTestRun() { + @Override + public void run(WhiteboxTestStage stage) throws Throwable { + + final Subscriber sub = stage.sub(); + boolean gotNPE = false; + try { + sub.onSubscribe(null); + } catch (final NullPointerException expected) { + gotNPE = true; + } + + assertTrue(gotNPE, "onSubscribe(null) did not throw NullPointerException"); + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.13 + @Override @Test + public void required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberTest(new TestStageTestRun() { + @Override + public void run(WhiteboxTestStage stage) throws Throwable { + + final Subscriber sub = stage.sub(); + boolean gotNPE = false; + try { + sub.onNext(null); + } catch (final NullPointerException expected) { + gotNPE = true; + } + + assertTrue(gotNPE, "onNext(null) did not throw NullPointerException"); + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#2.13 + @Override @Test + public void required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { + subscriberTest(new TestStageTestRun() { + @Override + public void run(WhiteboxTestStage stage) throws Throwable { + + final Subscriber sub = stage.sub(); + boolean gotNPE = false; + try { + sub.onError(null); + } catch (final NullPointerException expected) { + gotNPE = true; + } finally { + assertTrue(gotNPE, "onError(null) did not throw NullPointerException"); + } + + env.verifyNoAsyncErrorsNoDelay(); + } + }); + } + + ////////////////////// SUBSCRIPTION SPEC RULE VERIFICATION ////////////////// - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.1 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.1 @Override @Test public void untested_spec301_mustNotBeCalledOutsideSubscriberContext() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.8 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.8 @Override @Test public void required_spec308_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable { subscriberTest(new TestStageTestRun() { @@ -369,31 +448,31 @@ public void run(WhiteboxTestStage stage) throws InterruptedException { }); } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.10 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.10 @Override @Test public void untested_spec310_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.11 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.11 @Override @Test public void untested_spec311_requestMaySynchronouslyCallOnCompleteOrOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.14 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.14 @Override @Test public void untested_spec314_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.15 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.15 @Override @Test public void untested_spec315_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? } - // Verifies rule: https://github.com/reactive-streams/reactive-streams#3.16 + // Verifies rule: https://github.com/reactive-streams/reactive-streams-jvm#3.16 @Override @Test public void untested_spec316_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception { notVerified(); // cannot be meaningfully tested, or can it? @@ -407,11 +486,24 @@ abstract class TestStageTestRun { public abstract void run(WhiteboxTestStage stage) throws Throwable; } + /** + * Prepares subscriber and publisher pair (by subscribing the first to the latter), + * and then hands over the tests {@link WhiteboxTestStage} over to the test. + * + * The test stage is, like in a puppet show, used to orchestrate what each participant should do. + * Since this is a whitebox test, this allows the stage to completely control when and how to signal / expect signals. + */ public void subscriberTest(TestStageTestRun body) throws Throwable { WhiteboxTestStage stage = new WhiteboxTestStage(env, true); body.run(stage); } + /** + * Provides a {@link WhiteboxTestStage} without performing any additional setup, + * like the {@link #subscriberTest(SubscriberWhiteboxVerification.TestStageTestRun)} would. + * + * Use this method to write tests in which you need full control over when and how the initial {@code subscribe} is signalled. + */ public void subscriberTestWithoutSetup(TestStageTestRun body) throws Throwable { WhiteboxTestStage stage = new WhiteboxTestStage(env, false); body.run(stage); @@ -471,7 +563,10 @@ public WhiteboxSubscriberProbe createWhiteboxSubscriberProbe(TestEnvironment } public T signalNext() throws InterruptedException { - T element = nextT(); + return signalNext(nextT()); + } + + private T signalNext(T element) throws InterruptedException { sendNext(element); return element; } @@ -489,7 +584,7 @@ public void verifyNoAsyncErrors() { /** * This class is intented to be used as {@code Subscriber} decorator and should be used in {@code pub.subscriber(...)} calls, * in order to allow intercepting calls on the underlying {@code Subscriber}. - * This delegation allows the proxy to implement {@link org.reactivestreams.tck.SubscriberWhiteboxVerification.BlackboxProbe} assertions. + * This delegation allows the proxy to implement {@link BlackboxProbe} assertions. */ public static class BlackboxSubscriberProxy extends BlackboxProbe implements Subscriber { @@ -628,7 +723,7 @@ public void expectError(Throwable expected, long timeoutMillis) throws Interrupt } public void expectNone() throws InterruptedException { - expectNone(env.defaultTimeoutMillis()); + expectNone(env.defaultNoSignalsTimeoutMillis()); } public void expectNone(long withinMillis) throws InterruptedException { @@ -653,8 +748,6 @@ private SubscriberPuppet puppet() { public void registerOnSubscribe(SubscriberPuppet p) { if (!puppet.isCompleted()) { puppet.complete(p); - } else { - env.flop(String.format("Subscriber %s illegally accepted a second Subscription", sub())); } } @@ -688,9 +781,21 @@ public interface SubscriberProbe { } + /** + * Implement this puppet in your Whitebox style tests. + * The test suite will invoke the specific trigger/signal methods requesting you to execute the specific action. + * Since this is a whitebox style test, you're allowed and expected to use knowladge about your implementation to + * make implement these calls. + */ public interface SubscriberPuppet { + /** + * Trigger {@code request(elements)} on your {@link Subscriber} + */ void triggerRequest(long elements); + /** + * Trigger {@code cancel()} on your {@link Subscriber} + */ void signalCancel(); } diff --git a/tck/src/main/java/org/reactivestreams/tck/TestEnvironment.java b/tck/src/main/java/org/reactivestreams/tck/TestEnvironment.java index edbb03cf..1d8cf062 100644 --- a/tck/src/main/java/org/reactivestreams/tck/TestEnvironment.java +++ b/tck/src/main/java/org/reactivestreams/tck/TestEnvironment.java @@ -1,12 +1,23 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.reactivestreams.tck.support.SubscriberBufferOverflowException; -import org.reactivestreams.tck.support.Optional; +import org.reactivestreams.tck.flow.support.SubscriberBufferOverflowException; +import org.reactivestreams.tck.flow.support.Optional; -import java.text.NumberFormat; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; @@ -23,7 +34,10 @@ public class TestEnvironment { private static final String DEFAULT_TIMEOUT_MILLIS_ENV = "DEFAULT_TIMEOUT_MILLIS"; private static final long DEFAULT_TIMEOUT_MILLIS = 100; + private static final String DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV = "DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS"; + private final long defaultTimeoutMillis; + private final long defaultNoSignalsTimeoutMillis; private final boolean printlnDebug; private CopyOnWriteArrayList asyncErrors = new CopyOnWriteArrayList(); @@ -33,16 +47,29 @@ public class TestEnvironment { * interactions. Longer timeout does not invalidate the correctness of * the implementation, but can in some cases result in longer time to * run the tests. - * * @param defaultTimeoutMillis default timeout to be used in all expect* methods + * @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore * @param printlnDebug if true, signals such as OnNext / Request / OnComplete etc will be printed to standard output, - * often helpful to pinpoint simple race conditions etc. */ - public TestEnvironment(long defaultTimeoutMillis, boolean printlnDebug) { + public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis, boolean printlnDebug) { this.defaultTimeoutMillis = defaultTimeoutMillis; + this.defaultNoSignalsTimeoutMillis = defaultNoSignalsTimeoutMillis; this.printlnDebug = printlnDebug; } + /** + * Tests must specify the timeout for expected outcome of asynchronous + * interactions. Longer timeout does not invalidate the correctness of + * the implementation, but can in some cases result in longer time to + * run the tests. + * + * @param defaultTimeoutMillis default timeout to be used in all expect* methods + * @param defaultNoSignalsTimeoutMillis default timeout to be used when no further signals are expected anymore + */ + public TestEnvironment(long defaultTimeoutMillis, long defaultNoSignalsTimeoutMillis) { + this(defaultTimeoutMillis, defaultNoSignalsTimeoutMillis, false); + } + /** * Tests must specify the timeout for expected outcome of asynchronous * interactions. Longer timeout does not invalidate the correctness of @@ -52,7 +79,7 @@ public TestEnvironment(long defaultTimeoutMillis, boolean printlnDebug) { * @param defaultTimeoutMillis default timeout to be used in all expect* methods */ public TestEnvironment(long defaultTimeoutMillis) { - this(defaultTimeoutMillis, false); + this(defaultTimeoutMillis, defaultTimeoutMillis, false); } /** @@ -68,7 +95,7 @@ public TestEnvironment(long defaultTimeoutMillis) { * often helpful to pinpoint simple race conditions etc. */ public TestEnvironment(boolean printlnDebug) { - this(envDefaultTimeoutMillis(), printlnDebug); + this(envDefaultTimeoutMillis(), envDefaultNoSignalsTimeoutMillis(), printlnDebug); } /** @@ -81,13 +108,22 @@ public TestEnvironment(boolean printlnDebug) { * or the default value ({@link TestEnvironment#DEFAULT_TIMEOUT_MILLIS}) will be used. */ public TestEnvironment() { - this(envDefaultTimeoutMillis()); + this(envDefaultTimeoutMillis(), envDefaultNoSignalsTimeoutMillis()); } + /** This timeout is used when waiting for a signal to arrive. */ public long defaultTimeoutMillis() { return defaultTimeoutMillis; } + /** + * This timeout is used when asserting that no further signals are emitted. + * Note that this timeout default + */ + public long defaultNoSignalsTimeoutMillis() { + return defaultNoSignalsTimeoutMillis; + } + /** * Tries to parse the env variable {@code DEFAULT_TIMEOUT_MILLIS} as long and returns the value if present OR its default value. * @@ -98,16 +134,31 @@ public static long envDefaultTimeoutMillis() { if (envMillis == null) return DEFAULT_TIMEOUT_MILLIS; else try { return Long.parseLong(envMillis); - } catch(NumberFormatException ex) { + } catch (NumberFormatException ex) { throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", DEFAULT_TIMEOUT_MILLIS_ENV, envMillis), ex); } } + /** + * Tries to parse the env variable {@code DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS} as long and returns the value if present OR its default value. + * + * @throws java.lang.IllegalArgumentException when unable to parse the env variable + */ + public static long envDefaultNoSignalsTimeoutMillis() { + final String envMillis = System.getenv(DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV); + if (envMillis == null) return envDefaultTimeoutMillis(); + else try { + return Long.parseLong(envMillis); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException(String.format("Unable to parse %s env value [%s] as long!", DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS_ENV, envMillis), ex); + } + } + /** * To flop means to "fail asynchronously", either by onErroring or by failing some TCK check triggered asynchronously. * This method does *NOT* fail the test - it's up to inspections of the error to fail the test if required. * - * Use {@code env.verifyNoAsyncErrors()} at the end of your TCK tests to verify there no flops called during it's execution. + * Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution. * To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly * from the environment using {@code env.dropAsyncError()}. * @@ -127,7 +178,7 @@ public void flop(String msg) { * * This overload keeps the passed in throwable as the asyncError, instead of creating an AssertionError for this. * - * Use {@code env.verifyNoAsyncErrors()} at the end of your TCK tests to verify there no flops called during it's execution. + * Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution. * To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly * from the environment using {@code env.dropAsyncError()}. * @@ -147,7 +198,7 @@ public void flop(Throwable thr, String msg) { * * This overload keeps the passed in throwable as the asyncError, instead of creating an AssertionError for this. * - * Use {@code env.verifyNoAsyncErrors()} at the end of your TCK tests to verify there no flops called during it's execution. + * Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution. * To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly * from the environment using {@code env.dropAsyncError()}. * @@ -167,7 +218,7 @@ public void flop(Throwable thr) { * This method DOES fail the test right away (it tries to, by throwing an AssertionException), * in such it is different from {@link org.reactivestreams.tck.TestEnvironment#flop} which only records the error. * - * Use {@code env.verifyNoAsyncErrors()} at the end of your TCK tests to verify there no flops called during it's execution. + * Use {@code env.verifyNoAsyncErrorsNoDelay()} at the end of your TCK tests to verify there no flops called during it's execution. * To check investigate asyncErrors more closely you can use {@code expectError} methods or collect the error directly * from the environment using {@code env.dropAsyncError()}. * @@ -192,7 +243,7 @@ public void subscribe(Publisher pub, TestSubscriber sub) throws Interr public void subscribe(Publisher pub, TestSubscriber sub, long timeoutMillis) throws InterruptedException { pub.subscribe(sub); sub.subscription.expectCompletion(timeoutMillis, String.format("Could not subscribe %s to Publisher %s", sub, pub)); - verifyNoAsyncErrors(); + verifyNoAsyncErrorsNoDelay(); } public ManualSubscriber newBlackholeSubscriber(Publisher pub) throws InterruptedException { @@ -223,16 +274,39 @@ public Throwable dropAsyncError() { } } + /** + * Waits for {@link TestEnvironment#defaultTimeoutMillis()} and then verifies that no asynchronous errors + * were signalled pior to, or during that time (by calling {@code flop()}). + */ + public void verifyNoAsyncErrors() { + verifyNoAsyncErrors(defaultNoSignalsTimeoutMillis()); + } + + /** + * This version of {@code verifyNoAsyncErrors} should be used when errors still could be signalled + * asynchronously during {@link TestEnvironment#defaultTimeoutMillis()} time. + *

+ * It will immediatly check if any async errors were signaled (using {@link TestEnvironment#flop(String)}, + * and if no errors encountered wait for another default timeout as the errors may yet be signalled. + * The initial check is performed in order to fail-fast in case of an already failed test. + */ public void verifyNoAsyncErrors(long delay) { try { + verifyNoAsyncErrorsNoDelay(); + Thread.sleep(delay); - verifyNoAsyncErrors(); + verifyNoAsyncErrorsNoDelay(); } catch (InterruptedException e) { throw new RuntimeException(e); } } - public void verifyNoAsyncErrors() { + /** + * Verifies that no asynchronous errors were signalled pior to calling this method (by calling {@code flop()}). + * This version of verifyNoAsyncError does not wait before checking for asynchronous errors, and is to be used + * for example in tight loops etc. + */ + public void verifyNoAsyncErrorsNoDelay() { for (Throwable e : asyncErrors) { if (e instanceof AssertionError) { throw (AssertionError) e; @@ -267,6 +341,9 @@ public Optional findCallerMethodInStackTrace(String method) { // ---- classes ---- + /** + * {@link Subscriber} implementation which can be steered by test code and asserted on. + */ public static class ManualSubscriber extends TestSubscriber { Receptacle received; @@ -314,6 +391,10 @@ public T requestNextElement(long timeoutMillis, String errorMsg) throws Interrup return nextElement(timeoutMillis, errorMsg); } + public Optional requestNextElementOrEndOfStream() throws InterruptedException { + return requestNextElementOrEndOfStream(env.defaultTimeoutMillis(), "Did not receive expected stream completion"); + } + public Optional requestNextElementOrEndOfStream(String errorMsg) throws InterruptedException { return requestNextElementOrEndOfStream(env.defaultTimeoutMillis(), errorMsg); } @@ -433,14 +514,24 @@ public void expectCompletion(long timeoutMillis, String errorMsg) throws Interru public void expectErrorWithMessage(Class expected, String requiredMessagePart) throws Exception { expectErrorWithMessage(expected, requiredMessagePart, env.defaultTimeoutMillis()); } + public void expectErrorWithMessage(Class expected, List requiredMessagePartAlternatives) throws Exception { + expectErrorWithMessage(expected, requiredMessagePartAlternatives, env.defaultTimeoutMillis()); + } @SuppressWarnings("ThrowableResultOfMethodCallIgnored") public void expectErrorWithMessage(Class expected, String requiredMessagePart, long timeoutMillis) throws Exception { + expectErrorWithMessage(expected, Collections.singletonList(requiredMessagePart), timeoutMillis); + } + public void expectErrorWithMessage(Class expected, List requiredMessagePartAlternatives, long timeoutMillis) throws Exception { final E err = expectError(expected, timeoutMillis); final String message = err.getMessage(); - assertTrue(message.contains(requiredMessagePart), + + boolean contains = false; + for (String requiredMessagePart : requiredMessagePartAlternatives) + if (message.contains(requiredMessagePart)) contains = true; // not short-circuting loop, it is expected to + assertTrue(contains, String.format("Got expected exception [%s] but missing message part [%s], was: %s", - err.getClass(), requiredMessagePart, err.getMessage())); + err.getClass(), "anyOf: " + requiredMessagePartAlternatives, err.getMessage())); } public E expectError(Class expected) throws Exception { @@ -460,11 +551,11 @@ public E expectError(Class expected, long timeoutMillis } public void expectNone() throws InterruptedException { - expectNone(env.defaultTimeoutMillis()); + expectNone(env.defaultNoSignalsTimeoutMillis()); } public void expectNone(String errMsgPrefix) throws InterruptedException { - expectNone(env.defaultTimeoutMillis(), errMsgPrefix); + expectNone(env.defaultNoSignalsTimeoutMillis(), errMsgPrefix); } public void expectNone(long withinMillis) throws InterruptedException { @@ -699,6 +790,10 @@ public void expectCancelling() throws InterruptedException { public void expectCancelling(long timeoutMillis) throws InterruptedException { cancelled.expectClose(timeoutMillis, "Did not receive expected cancelling of upstream subscription"); } + + public boolean isCancelled() throws InterruptedException { + return cancelled.isClosed(); + } } /** diff --git a/tck/src/main/java/org/reactivestreams/tck/WithHelperPublisher.java b/tck/src/main/java/org/reactivestreams/tck/WithHelperPublisher.java index a2a1653b..6deb052d 100644 --- a/tck/src/main/java/org/reactivestreams/tck/WithHelperPublisher.java +++ b/tck/src/main/java/org/reactivestreams/tck/WithHelperPublisher.java @@ -1,9 +1,20 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; -import org.reactivestreams.tck.support.Function; -import org.reactivestreams.tck.support.HelperPublisher; -import org.reactivestreams.tck.support.InfiniteHelperPublisher; +import org.reactivestreams.tck.flow.support.Function; +import org.reactivestreams.tck.flow.support.HelperPublisher; +import org.reactivestreams.tck.flow.support.InfiniteHelperPublisher; import java.util.concurrent.ExecutorService; diff --git a/tck/src/main/java/org/reactivestreams/tck/flow/support/Function.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/Function.java new file mode 100644 index 00000000..0b6724f9 --- /dev/null +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/Function.java @@ -0,0 +1,16 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +public interface Function { + public Out apply(In in) throws Throwable; +} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/HelperPublisher.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/HelperPublisher.java similarity index 60% rename from tck/src/main/java/org/reactivestreams/tck/support/HelperPublisher.java rename to tck/src/main/java/org/reactivestreams/tck/flow/support/HelperPublisher.java index ba0fce68..1fd96359 100644 --- a/tck/src/main/java/org/reactivestreams/tck/support/HelperPublisher.java +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/HelperPublisher.java @@ -1,11 +1,20 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; import java.util.Collections; import java.util.Iterator; import java.util.concurrent.Executor; -import org.reactivestreams.Subscription; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Publisher; + import org.reactivestreams.example.unicast.AsyncIterablePublisher; public class HelperPublisher extends AsyncIterablePublisher { @@ -30,4 +39,4 @@ public HelperPublisher(final int from, final int to, final Function } }, executor); } -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/InfiniteHelperPublisher.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/InfiniteHelperPublisher.java similarity index 57% rename from tck/src/main/java/org/reactivestreams/tck/support/InfiniteHelperPublisher.java rename to tck/src/main/java/org/reactivestreams/tck/flow/support/InfiniteHelperPublisher.java index 94897e4b..93227526 100644 --- a/tck/src/main/java/org/reactivestreams/tck/support/InfiniteHelperPublisher.java +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/InfiniteHelperPublisher.java @@ -1,4 +1,15 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; import org.reactivestreams.example.unicast.AsyncIterablePublisher; @@ -27,4 +38,4 @@ public InfiniteHelperPublisher(final Function create, final Executor } }, executor); } -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/NonFatal.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/NonFatal.java similarity index 53% rename from tck/src/main/java/org/reactivestreams/tck/support/NonFatal.java rename to tck/src/main/java/org/reactivestreams/tck/flow/support/NonFatal.java index b6dedd4b..13fbc0d3 100644 --- a/tck/src/main/java/org/reactivestreams/tck/support/NonFatal.java +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/NonFatal.java @@ -1,4 +1,15 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; /** diff --git a/tck/src/main/java/org/reactivestreams/tck/support/Optional.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/Optional.java similarity index 61% rename from tck/src/main/java/org/reactivestreams/tck/support/Optional.java rename to tck/src/main/java/org/reactivestreams/tck/flow/support/Optional.java index 82caa985..b1c53287 100644 --- a/tck/src/main/java/org/reactivestreams/tck/support/Optional.java +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/Optional.java @@ -1,4 +1,15 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; import java.util.NoSuchElementException; diff --git a/tck/src/main/java/org/reactivestreams/tck/flow/support/PublisherVerificationRules.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/PublisherVerificationRules.java new file mode 100644 index 00000000..7ef44b2b --- /dev/null +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/PublisherVerificationRules.java @@ -0,0 +1,646 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +/** + * Internal TCK use only. + * Add / Remove tests for PublisherVerification here to make sure that they arre added/removed in the other places. + */ +public interface PublisherVerificationRules { + /** + * Validates that the override of {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} + * returns a non-negative value. + */ + void required_validate_maxElementsFromPublisher() throws Exception; + /** + * Validates that the override of {@link org.reactivestreams.tck.PublisherVerification#boundedDepthOfOnNextAndRequestRecursion()} + * returns a positive value. + */ + void required_validate_boundedDepthOfOnNextAndRequestRecursion() throws Exception; + /** + * Asks for a {@code Publisher} that should emit exactly one item and complete (both within a + * timeout specified by {@link org.reactivestreams.tck.TestEnvironment#defaultTimeoutMillis()}) + * in response to a request(1). + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} returns zero. + * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code Publisher.subscribe(Subscriber)} method has actual implementation,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, if there is an upstream {@code Publisher}, + * that {@code Publisher} is actually subscribed to,
  • + *
  • if the {@code Publisher} is part of a chain, all elements actually issue a {@code request()} call + * in response to the test subscriber or by default to their upstream,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, the {@code Subscriber.onSubscribe} is called + * as part of the preparation process (usually before subscribing to other {@code Publisher}s),
  • + *
  • if the {@code Publisher} implementation works for a consumer that calls {@code request(1)},
  • + *
  • if the {@code Publisher} implementation is able to emit an {@code onComplete} without requests,
  • + *
  • that the {@code Publisher} implementation does not emit more than the allowed elements (exactly one).
  • + *
+ */ + void required_createPublisher1MustProduceAStreamOfExactly1Element() throws Throwable; + /** + * Asks for a {@code Publisher} that should emit exactly three items and complete (all within a + * timeout specified by {@link org.reactivestreams.tck.TestEnvironment#defaultTimeoutMillis()}). + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * The tests requests one-by-one and verifies each single response item arrives in time. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code Publisher.subscribe(Subscriber)} method has actual implementation,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, if there is an upstream {@code Publisher}, + * that {@code Publisher} is actually subscribed to,
  • + *
  • if the {@code Publisher} is part of a chain, all elements actually issue a {@code request()} call + * in response to the test subscriber or by default to their upstream,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, the {@code Subscriber.onSubscribe} is called + * as part of the preparation process (usually before subscribing to other {@code Publisher}s),
  • + *
  • if the {@code Publisher} implementation works for a subscriber that calls {@code request(1)} after consuming an item,
  • + *
  • if the {@code Publisher} implementation is able to emit an {@code onComplete} without requests.
  • + *
+ */ + void required_createPublisher3MustProduceAStreamOfExactly3Elements() throws Throwable; + /** + * Asks for a {@code Publisher} that responds to a request pattern of 0 (not requesting upfront), 1, 1 and 2 + * in a timely manner. + *

+ * Verifies rule: 1.1 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 5. + *

+ * This test ensures that the {@code Publisher} implementation correctly responds to {@code request()} calls that in + * total are less than the number of elements this {@code Publisher} could emit (thus the completion event won't be emitted). + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable; + /** + * Asks for a short {@code Publisher} and verifies that requesting once and with more than the length (but bounded) results in the + * correct number of items to be emitted (i.e., length 3 and request 10) followed by an {@code onComplete} signal. + *

+ * Verifies rule: 1.2 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * This test ensures that the {@code Publisher} implementation can deal with larger requests than the number of items it can produce. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass.
  • + *
+ */ + void required_spec102_maySignalLessThanRequestedAndTerminateSubscription() throws Throwable; + /** + * Asks for a short {@code Publisher} (i.e., length 10), repeatedly subscribes to this {@code Publisher}, requests items + * one by one and verifies the {@code Publisher} calls the {@code onXXX} methods non-overlappingly. + *

+ * Verifies rule: 1.3 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 10. + *

+ * Note that this test is probabilistic, that is, may not capture any concurrent invocation in a {code Publisher} implementation. + * Note also that this test is sensitive to cases when a {@code request()} call in {@code onSubscribe()} triggers an asynchronous + * call to the other {@code onXXX} methods. In contrast, the test allows synchronous call chain of + * {@code onSubscribe -> request -> onNext}. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if a {@code request()} call from {@code onSubscribe()} could trigger an asynchronous call to {@code onNext()} and if so, make sure + * such {@code request()} calls are deferred until the call to {@code onSubscribe()} returns normally.
  • + *
+ */ + void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable; + /** + * Asks for an error {@code Publisher} that should call {@code onSubscribe} exactly once + * followed by a single call to {@code onError()} without receiving any requests and otherwise + * not throwing any exception. + *

+ * Verifies rule: 1.4 + *

+ * The test is not executed if {@code PublisherVerification.createErrorPublisher()} returns null. + *

+ * If this test fails, the following could be checked within the error {@code Publisher} implementation: + *

    + *
  • the {@code Publisher.subscribe(Subscriber)} method has actual implementation,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, if there is an upstream {@code Publisher}, + * that {@code Publisher} is actually subscribed to,
  • + *
  • if the {@code Publisher} implementation does signal an {@code onSubscribe} before signalling {@code onError},
  • + *
  • if the {@code Publisher} implementation is able to emit an {@code onError} without requests,
  • + *
  • if the {@code Publisher} is non-empty as this test requires a {@code Publisher} to signal an + * {@code onError} eagerly.
  • + *
+ */ + void optional_spec104_mustSignalOnErrorWhenFails() throws Throwable; + /** + * Asks for a short {@code Publisher} (i.e., length 3) and verifies, after requesting one by one, the sequence + * completes normally. + *

+ * Verifies rule: 1.5 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * Note that the tests requests 1 after the items have been received and before expecting an {@code onComplete} signal. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
+ */ + void required_spec105_mustSignalOnCompleteWhenFiniteStreamTerminates() throws Throwable; + /** + * Asks for an empty {@code Publisher} (i.e., length 0) and verifies it completes in a timely manner. + *

+ * Verifies rule: 1.5 + *

+ * Note that the tests requests 1 before expecting an {@code onComplete} signal. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • if the {@code Publisher} is non-empty as this test requires a {@code Publisher} without items.
  • + *
+ */ + void optional_spec105_emptyStreamMustTerminateBySignallingOnComplete() throws Throwable; + /** + * Currently, this test is skipped because it is unclear this rule can be effectively checked + * on a {@code Publisher} instance without looking into or hooking into the implementation of it. + *

+ * Verifies rule: 1.6 + */ + void untested_spec106_mustConsiderSubscriptionCancelledAfterOnErrorOrOnCompleteHasBeenCalled() throws Throwable; + /** + * Asks for a single-element {@code Publisher} and checks if requesting after the terminal event doesn't + * lead to more items or terminal signals to be emitted. + *

+ * Verifies rule: 1.7 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 1. + *

+ * The tests requests more items than the expected {@code Publisher} length upfront and some more items after its completion. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • the indication for the terminal state is properly persisted and a request call can't trigger emission of more items or another + * terminal signal.
  • + *
+ */ + void required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled() throws Throwable; + /** + * Currently, this test is skipped, although it is possible to validate an error {@code Publisher} along + * the same lines as {@link #required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled()}. + *

+ * Verifies rule: 1.7 + */ + void untested_spec107_mustNotEmitFurtherSignalsOnceOnErrorHasBeenSignalled() throws Throwable; + /** + * Currently, this test is skipped because there was no agreement on how to verify its "eventually" requirement. + *

+ * Verifies rule: 1.8 + */ + void untested_spec108_possiblyCanceledSubscriptionShouldNotReceiveOnErrorOrOnCompleteSignals() throws Throwable; + /** + * Asks for an empty {@code Publisher} and verifies if {@code onSubscribe} signal was emitted before + * any other {@code onNext}, {@code onError} or {@code onComplete} signal. + *

+ * Verifies rule: 1.9 + *

+ * Note that this test doesn't request anything, however, an {@code onNext} is not considered as a failure. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • the {@code Publisher.subscribe(Subscriber)} method has actual implementation,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, if there is an upstream {@code Publisher}, + * that {@code Publisher} is actually subscribed to,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, the {@code Subscriber.onSubscribe} is called + * as part of the preparation process (usually before subscribing to other {@code Publisher}s).
  • + *
+ */ + void required_spec109_mustIssueOnSubscribeForNonNullSubscriber() throws Throwable; + /** + * Currently, this test is skipped because there is no common agreement on what is to be considered a fatal exception and + * besides, {@code Publisher.subscribe} is only allowed throw a {@code NullPointerException} and any other + * exception would require looking into or hooking into the implementation of the {@code Publisher}. + *

+ * Verifies rule: 1.9 + */ + void untested_spec109_subscribeShouldNotThrowNonFatalThrowable() throws Throwable; + /** + * Asks for an empty {@code Publisher} and calls {@code subscribe} on it with {@code null} that should result in + * a {@code NullPointerException} to be thrown. + *

+ * Verifies rule: 1.9 + *

+ * If this test fails, check if the {@code subscribe()} implementation has an explicit null check (or a method dereference + * on the {@code Subscriber}), especially if the incoming {@code Subscriber} is wrapped or stored to be used later. + */ + void required_spec109_subscribeThrowNPEOnNullSubscriber() throws Throwable; + /** + * Asks for an error {@code Publisher} that should call {@code onSubscribe} exactly once + * followed by a single call to {@code onError()} without receiving any requests. + *

+ * Verifies rule: 1.9 + *

+ * The test is not executed if {@code PublisherVerification.createErrorPublisher()} returns null. + *

+ * The difference between this test and {@link #optional_spec104_mustSignalOnErrorWhenFails()} is that there is + * no explicit verification if exceptions were thrown in addition to the regular {@code onSubscribe+onError} signal pair. + *

+ * If this test fails, the following could be checked within the error {@code Publisher} implementation: + *

    + *
  • the {@code Publisher.subscribe(Subscriber)} method has actual implementation,
  • + *
  • in the {@code Publisher.subscribe(Subscriber)} method, if there is an upstream {@code Publisher}, + * that {@code Publisher} is actually subscribed to,
  • + *
  • if the {@code Publisher} implementation is able to emit an {@code onError} without requests,
  • + *
  • if the {@code Publisher} is non-empty as this test expects a {@code Publisher} without items.
  • + *
+ */ + void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe() throws Throwable; + /** + * Currently, this test is skipped because enforcing rule §1.10 requires unlimited retention and reference-equal checks on + * all incoming {@code Subscriber} which is generally infeasible, plus reusing the same {@code Subscriber} instance is + * better detected (or ignored) inside {@code Subscriber.onSubscribe} when the method is called multiple times. + *

+ * Verifies rule: 1.10 + */ + void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice() throws Throwable; + /** + * Asks for a single-element {@code Publisher} and subscribes to it twice, without consuming with either + * {@code Subscriber} instance + * (i.e., no requests are issued). + *

+ * Verifies rule: 1.11 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 1. + *

+ * Note that this test ignores what signals the {@code Publisher} emits. Any exception thrown through non-regular + * means will indicate a skipped test. + */ + void optional_spec111_maySupportMultiSubscribe() throws Throwable; + /** + * Asks for a single-element {@code Publisher} and subscribes to it twice. + * Each {@code Subscriber} requests for 1 element and checks if onNext or onComplete signals was received. + *

+ * Verifies rule: 1.11, + * and depends on valid implementation of rule 1.5 + * in order to verify this. + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 1. + *

+ * Any exception thrown through non-regular means will indicate a skipped test. + */ + void optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 5), subscribes 3 {@code Subscriber}s to it, requests with different + * patterns and checks if all 3 received the same events in the same order. + *

+ * Verifies rule: 1.11 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 5. + *

+ * The request pattern for the first {@code Subscriber} is (1, 1, 2, 1); for the second is (2, 3) and for the third is (3, 1, 1). + *

+ * Note that this test requires a {@code Publisher} that always emits the same signals to any {@code Subscriber}, regardless of + * when they subscribe and how they request elements. I.e., a "live" {@code Publisher} emitting the current time would not pass this test. + *

+ * Note that this test is optional and may appear skipped even if the behavior should be actually supported by the {@code Publisher}, + * see the skip message for an indication of this. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 3), subscribes 3 {@code Subscriber}s to it, requests more than the length items + * upfront with each and verifies they all received the same items in the same order (but does not verify they all complete). + *

+ * Verifies rule: 1.11 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * Note that this test requires a {@code Publisher} that always emits the same signals to any {@code Subscriber}, regardless of + * when they subscribe and how they request elements. I.e., a "live" {@code Publisher} emitting the current time would not pass this test. + *

+ * Note that this test is optional and may appear skipped even if the behavior should be actually supported by the {@code Publisher}, + * see the skip message for an indication of this. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 3), subscribes 3 {@code Subscriber}s to it, requests more than the length items + * upfront with each and verifies they all received the same items in the same order followed by an {@code onComplete} signal. + *

+ * Verifies rule: 1.11 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * Note that this test requires a {@code Publisher} that always emits the same signals to any {@code Subscriber}, regardless of + * when they subscribe and how they request elements. I.e., a "live" {@code Publisher} emitting the current time would not pass this test. + *

+ * Note that this test is optional and may appear skipped even if the behavior should be actually supported by the {@code Publisher}, + * see the skip message for an indication of this. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 6), requests several times from within {@code onSubscribe} and then requests + * one-by-one from {@code onNext}. + *

+ * Verifies rule: 3.2 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 6. + *

+ * The request pattern is 3 x 1 from within {@code onSubscribe} and one from within each {@code onNext} invocation. + *

+ * The test consumes the {@code Publisher} but otherwise doesn't verify the {@code Publisher} completes (however, it checks + * for errors). + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void required_spec302_mustAllowSynchronousRequestCallsFromOnNextAndOnSubscribe() throws Throwable; + /** + * Asks for a {@code Publisher} with length equal to the value returned by {@link #required_validate_boundedDepthOfOnNextAndRequestRecursion()} plus 1, + * calls {@code request(1)} externally and then from within {@code onNext} and checks if the stack depth did not increase beyond the + * amount permitted by {@link #required_validate_boundedDepthOfOnNextAndRequestRecursion()}. + *

+ * Verifies rule: 3.3 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than + * {@link #required_validate_boundedDepthOfOnNextAndRequestRecursion()} plus 1. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the implementation doesn't allow unbounded recursion when {@code request()} is called from within {@code onNext}, i.e., the lack of + * reentrant-safe state machine around the request amount (such as a for loop with a bound on the parameter {@code n} that calls {@code onNext}). + *
+ */ + void required_spec303_mustNotAllowUnboundedRecursion() throws Throwable; + /** + * Currently, this test is skipped because a {@code request} could enter into a synchronous computation via {@code onNext} + * legally and otherwise there is no common agreement how to detect such heavy computation reliably. + *

+ * Verifies rule: 3.4 + */ + void untested_spec304_requestShouldNotPerformHeavyComputations() throws Exception; + /** + * Currently, this test is skipped because there is no reliable agreed upon way to detect a heavy computation. + *

+ * Verifies rule: 3.5 + */ + void untested_spec305_cancelMustNotSynchronouslyPerformHeavyComputation() throws Exception; + /** + * Asks for a short {@code Publisher} (length 3) and verifies that cancelling without requesting anything, then requesting + * items should result in no signals to be emitted. + *

+ * Verifies rule: 3.6 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * The post-cancellation request pattern is (1, 1, 1). + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the cancellation indicator flag is properly persisted (may require volatile) and checked as part of the signal emission process.
  • + *
+ */ + void required_spec306_afterSubscriptionIsCancelledRequestMustBeNops() throws Throwable; + /** + * Asks for a single-element {@code Publisher} and verifies that without requesting anything, cancelling the sequence + * multiple times should result in no signals to be emitted and should result in an thrown exception. + *

+ * Verifies rule: 3.7 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 1. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the cancellation indicator flag is properly persisted (may require volatile) and checked as part of the signal emission process.
  • + *
+ */ + void required_spec307_afterSubscriptionIsCancelledAdditionalCancelationsMustBeNops() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 10) and issues a {@code request(0)} which should trigger an {@code onError} call + * with an {@code IllegalArgumentException}. + *

+ * Verifies rule: 3.9 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 10. + *

+ * Note that this test expects the {@code IllegalArgumentException} being signalled through {@code onError}, not by + * throwing from {@code request()} (which is also forbidden) or signalling the error by any other means (i.e., through the + * {@code Thread.currentThread().getUncaughtExceptionHandler()} for example). + *

+ * Note also that requesting and emission may happen concurrently and honoring this rule may require extra coordination within + * the {@code Publisher}. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the {@code Publisher} can emit an {@code onError} in this particular case, even if there was no prior and legal + * {@code request} call and even if the {@code Publisher} would like to emit items first before emitting an {@code onError} + * in general. + *
+ */ + void required_spec309_requestZeroMustSignalIllegalArgumentException() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 10) and issues a random, negative {@code request()} call which should + * trigger an {@code onError} call with an {@code IllegalArgumentException}. + *

+ * Verifies rule: 3.9 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 10. + *

+ * Note that this test expects the {@code IllegalArgumentException} being signalled through {@code onError}, not by + * throwing from {@code request()} (which is also forbidden) or signalling the error by any other means (i.e., through the + * {@code Thread.currentThread().getUncaughtExceptionHandler()} for example). + *

+ * Note also that requesting and emission may happen concurrently and honoring this rule may require extra coordination within + * the {@code Publisher}. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the {@code Publisher} can emit an {@code onError} in this particular case, even if there was no prior and legal + * {@code request} call and even if the {@code Publisher} would like to emit items first before emitting an {@code onError} + * in general. + *
+ */ + void required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 10) and issues a random, negative {@code request()} call which should + * trigger an {@code onError} call with an {@code IllegalArgumentException}. + *

+ * Verifies rule: 3.9 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 10. + *

+ * Note that this test expects the {@code IllegalArgumentException} being signalled through {@code onError}, not by + * throwing from {@code request()} (which is also forbidden) or signalling the error by any other means (i.e., through the + * {@code Thread.currentThread().getUncaughtExceptionHandler()} for example). + *

+ * Note also that requesting and emission may happen concurrently and honoring this rule may require extra coordination within + * the {@code Publisher}. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the {@code Publisher} can emit an {@code onError} in this particular case, even if there was no prior and legal + * {@code request} call and even if the {@code Publisher} would like to emit items first before emitting an {@code onError} + * in general. + *
+ */ + void optional_spec309_requestNegativeNumberMaySignalIllegalArgumentExceptionWithSpecificMessage() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 20), requests some items (less than the length), consumes one item then + * cancels the sequence and verifies the publisher emitted at most the requested amount and stopped emitting (or terminated). + *

+ * Verifies rule: 3.12 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 20. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the cancellation indicator flag is properly persisted (may require volatile) and checked as part of the signal emission process.
  • + *
+ */ + void required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 3) requests and consumes one element from it, cancels the {@code Subscription} + * , calls {@code System.gc()} and then checks if all references to the test {@code Subscriber} has been dropped (by checking + * the {@code WeakReference} has been emptied). + *

+ * Verifies rule: 3.13 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • the cancellation indicator flag is properly persisted (may require volatile) and checked as part of the signal emission process.
  • + *
  • the {@code Publisher} stores the {@code Subscriber} reference somewhere which is then not cleaned up when the {@code Subscriber} is cancelled. + * Note that this may happen on many code paths in a {@code Publisher}, for example in an emission loop that terminates because of the + * {@code cancel} signal or because reaching a terminal state. Note also that eagerly nulling {@code Subscriber} references may not be necessary + * for this test to pass in case there is a self-contained chain of them (i.e., {@code Publisher.subscribe()} creates a chain of fresh + * {@code Subscriber} instances where each of them only references their downstream {@code Subscriber} thus the chain can get GC'd + * when the reference to the final {@code Subscriber} is dropped). + *
+ */ + void required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 3) and requests {@code Long.MAX_VALUE} from it, verifying that the + * {@code Publisher} emits all of its items and completes normally + * and does not keep spinning attempting to fulfill the {@code Long.MAX_VALUE} demand by some means. + *

+ * Verifies rule: 3.17 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void required_spec317_mustSupportAPendingElementCountUpToLongMaxValue() throws Throwable; + /** + * Asks for a short {@code Publisher} (length 3) and requests {@code Long.MAX_VALUE} from it in total (split across + * two {@code Long.MAX_VALUE / 2} and one {@code request(1)}), verifying that the + * {@code Publisher} emits all of its items and completes normally. + *

+ * Verifies rule: 3.17 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than 3. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} implements adding individual request amounts together properly (not overflowing into zero or negative pending request amounts) + * or not properly deducing the number of emitted items from the pending amount,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void required_spec317_mustSupportACumulativePendingElementCountUpToLongMaxValue() throws Throwable; + /** + * Asks for a very long {@code Publisher} (up to {@code Integer.MAX_VALUE}), requests {@code Long.MAX_VALUE - 1} after + * each received item and expects no failure due to a potential overflow in the pending emission count while consuming + * 10 items and cancelling the sequence. + *

+ * Verifies rule: 3.17 + *

+ * The test is not executed if {@link org.reactivestreams.tck.PublisherVerification#maxElementsFromPublisher()} is less than {@code Integer.MAX_VALUE}. + *

+ * The request pattern is one {@code request(1)} upfront and ten {@code request(Long.MAX_VALUE - 1)} after. + *

+ * If this test fails, the following could be checked within the {@code Publisher} implementation: + *

    + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Publisher} has some time-delay behavior,
  • + *
  • make sure the {@link #required_createPublisher1MustProduceAStreamOfExactly1Element()} and {@link #required_createPublisher3MustProduceAStreamOfExactly3Elements()} tests pass,
  • + *
  • if the {@code Publisher} implementation considers the cumulative request amount it receives,
  • + *
  • if the {@code Publisher} implements adding individual request amounts together properly (not overflowing into zero or negative pending request amounts) + * or not properly deducing the number of emitted items from the pending amount,
  • + *
  • if the {@code Publisher} doesn't lose any {@code request()} signal and the state transition from idle -> emitting or emitting -> keep emitting works properly.
  • + *
+ */ + void required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue() throws Throwable; +} diff --git a/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBlackboxVerificationRules.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBlackboxVerificationRules.java new file mode 100644 index 00000000..e240c780 --- /dev/null +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBlackboxVerificationRules.java @@ -0,0 +1,383 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +import org.reactivestreams.tck.SubscriberBlackboxVerification; + +/** + * Internal TCK use only. + * Add / Remove tests for SubscriberBlackboxVerification here to make sure that they arre added/removed in the other places. + */ +public interface SubscriberBlackboxVerificationRules { + /** + * Asks for a {@code Subscriber} instance, expects it to call {@code request()} in + * a timely manner and signals as many {@code onNext} items as the very first request + * amount specified by the {@code Subscriber}. + *

+ * Verifies rule: 2.1 + *

+ * Notes: + *

    + *
  • This test emits the number of items requested thus the {@code Subscriber} implementation + * should not request too much.
  • + *
  • Only the very first {@code request} amount is considered.
  • + *
  • This test doesn't signal {@code onComplete} after the first set of {@code onNext} signals + * has been emitted and may cause resource leak in + * {@code Subscriber}s that expect a finite {@code Publisher}.
  • + *
  • The test ignores cancellation from the {@code Subscriber} and emits the requested amount regardless.
  • + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} requires external stimulus to begin requesting; override the + * {@link SubscriberBlackboxVerification#triggerRequest(org.reactivestreams.Subscriber)} method + * in this case,
  • + *
  • the {@code TestEnvironment} has large enough timeout specified in case the {@code Subscriber} has some time-delay behavior,
  • + *
  • if the {@code Subscriber} requests zero or a negative value in some circumstances,
  • + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onNext} methods. + *
+ */ + void required_spec201_blackbox_mustSignalDemandViaSubscriptionRequest() throws Throwable; + /** + * Currently, this test is skipped because there is no agreed upon approach how + * to detect if the {@code Subscriber} really goes async or just responds in + * a timely manner. + *

+ * Verifies rule: 2.2 + */ + void untested_spec202_blackbox_shouldAsynchronouslyDispatch() throws Exception; + /** + * Asks for a {@code Subscriber}, signals an {@code onSubscribe} followed by an {@code onComplete} synchronously, + * and checks if neither {@code request} nor {@code cancel} was called from within the {@code Subscriber}'s + * {@code onComplete} implementation. + *

+ * Verifies rule: 2.3 + *

+ * Notes: + *

    + *
  • The test checks for the presensce of method named "onComplete" in the current stacktrace when handling + * the {@code request} or {@code cancel} calls in the test's own {@code Subscription}. + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • no calls happen to {@code request} or {@code cancel} in response to an {@code onComplete} + * directly or indirectly,
  • + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onComplete} methods. + *
+ */ + void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable; + /** + * Asks for a {@code Subscriber}, signals an {@code onSubscribe} followed by an {@code onError} synchronously, + * and checks if neither {@code request} nor {@code cancel} was called from within the {@code Subscriber}'s + * {@code onComplete} implementation. + *

+ * Verifies rule: 2.3 + *

+ * Notes: + *

    + *
  • The test checks for the presensce of method named "onError" in the current stacktrace when handling + * the {@code request} or {@code cancel} calls in the test's own {@code Subscription}. + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • no calls happen to {@code request} or {@code cancel} in response to an {@code onError} + * directly or indirectly,
  • + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onError} methods. + *
+ */ + void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable; + /** + * Currently, this test is skipped because there is no way to check what the {@code Subscriber} "considers" + * since rule §2.3 forbids interaction from within the {@code onError} and {@code onComplete} methods. + *

+ * Verifies rule: 2.4 + *

+ * Notes: + *

    + *
  • It would be possible to check if there was an async interaction with the test's {@code Subscription} + * within a grace period but such check is still not generally decisive.
  • + *
+ */ + void untested_spec204_blackbox_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception; + /** + * Asks for a {@code Subscriber}, signals {@code onSubscribe} twice synchronously and expects the second {@code Subscription} gets + * cancelled in a timely manner and without any calls to its {@code request} method. + *

+ * Verifies rule: 2.5 + *

+ * Notes: + *

    + *
  • The test doesn't signal any other events than {@code onSubscribe} and may cause resource leak in + * {@code Subscriber}s that expect a finite {@code Publisher}. + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscribe.onSubscribe} implementation actually tries to detect multiple calls to it,
  • + *
  • if the second {@code Subscription} is cancelled asynchronously and that takes longer time than + * the {@code TestEnvironment}'s timeout permits.
  • + *
+ */ + void required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception; + + /** + * Currently, this test is skipped because it requires more control over the {@code Subscriber} implementation + * to make it cancel the {@code Subscription} for some external condition. + *

+ * Verifies rule: 2.6 + */ + void untested_spec206_blackbox_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception; + /** + * Currently, this test is skipped because it requires more control over the {@code Subscriber} implementation + * to issue requests based on external stimulus. + *

+ * Verifies rule: 2.7 + */ + void untested_spec207_blackbox_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception; + /** + * Currently, this test is skipped because there is no way to make the {@code Subscriber} implementation + * cancel the test's {@code Subscription} and check the outcome of sending {@code onNext}s after such + * cancel. + *

+ * Verifies rule: 2.8 + */ + void untested_spec208_blackbox_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable; + /** + * Asks for a {@code Subscriber}, expects it to request some amount and in turn be able to receive an {@code onComplete} + * synchronously from the {@code request} call without any {@code onNext} signals before that. + *

+ * Verifies rule: 2.9 + *

+ * Notes: + *

    + *
  • The test ignores cancellation from the {@code Subscriber}.
  • + *
  • Invalid request amounts are ignored by this test.
  • + *
  • Concurrent calls to the test's {@code Subscription.request()} must be externally synchronized, otherwise + * such case results probabilistically in multiple {@code onComplete} calls by the test.
  • + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onComplete} methods. + *
  • if the {@code Subscriber} requires external stimulus to begin requesting; override the + * {@link SubscriberBlackboxVerification#triggerRequest(org.reactivestreams.Subscriber)} method + * in this case,
  • + *
+ */ + void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable; + /** + * Asks for a {@code Subscriber} and expects it to handle {@code onComplete} independent of whether the {@code Subscriber} + * requests items or not. + *

+ * Verifies rule: 2.9 + *

+ * Notes: + *

    + *
  • Currently, the test doesn't call {@code onSubscribe} on the {@code Subscriber} which violates §1.9. + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onComplete} methods. + *
+ */ + void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable; + /** + * Asks for a {@code Subscriber}, signals {@code onSubscribe} followed by an {@code onError} synchronously. + *

+ * Verifies rule: 2.10 + *

+ * Notes: + *

    + *
  • Despite the method name, the test doesn't expect a request signal from {@code Subscriber} and emits the + * {@code onError} signal anyway. + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onError} methods. + *
+ */ + void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable; + + /** + * Asks for a {@code Subscriber}, signals {@code onSubscribe} followed by an {@code onError} synchronously. + *

+ * Verifies rule: 2.10 + *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws an unchecked exception from its {@code onSubscribe} or + * {@code onError} methods. + *
+ */ + void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall() throws Throwable; + + /** + * Currently, this test is skipped because it would require analyzing what the {@code Subscriber} implementation + * does. + *

+ * Verifies rule: 2.11 + */ + void untested_spec211_blackbox_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception; + /** + * Currently, this test is skipped because the test for + * {@link #required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal §2.5} + * is in a better position to test for handling the reuse of the same {@code Subscriber}. + *

+ * Verifies rule: 2.12 + *

+ * Notes: + *

    + *
  • In addition to §2.5, this rule could be better verified when testing a {@code Publisher}'s subscription behavior. + *
+ */ + void untested_spec212_blackbox_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality() throws Throwable; + /** + * Currently, this test is skipped because it would require more control over the {@code Subscriber} to + * fail internally in response to a set of legal event emissions, not throw any exception from the {@code Subscriber} + * methods and have it cancel the {@code Subscription}. + *

+ * Verifies rule: 2.13 + */ + void untested_spec213_blackbox_failingOnSignalInvocation() throws Exception; + /** + * Asks for a {@code Subscriber} and signals an {@code onSubscribe} event with {@code null} as a parameter and + * expects an immediate {@code NullPointerException} to be thrown by the {@code Subscriber.onSubscribe} method. + *

+ * Verifies rule: 2.13 + *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws a {@code NullPointerException} from its {@code onSubscribe} method + * in response to a {@code null} parameter and not some other unchecked exception or no exception at all. + *
+ */ + void required_spec213_blackbox_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; + /** + * Asks for a {@code Subscriber}, signals an {@code onSubscribe} event followed by a + * {@code onNext} with {@code null} as a parameter and + * expects an immediate {@code NullPointerException} to be thrown by the {@code Subscriber.onNext} method. + *

+ * Verifies rule: 2.13 + *

+ * Notes: + *

    + *
  • The test ignores cancellation and requests from the {@code Subscriber} and emits the {@code onNext} + * signal with a {@code null} parameter anyway.
  • + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws a {@code NullPointerException} from its {@code onNext} method + * in response to a {@code null} parameter and not some other unchecked exception or no exception at all. + *
+ */ + void required_spec213_blackbox_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; + /** + * Asks for a {@code Subscriber}, signals an {@code onSubscribe} event followed by a + * {@code onError} with {@code null} as a parameter and + * expects an immediate {@code NullPointerException} to be thrown by the {@code Subscriber.onError} method. + *

+ * Verifies rule: 2.13 + *

+ * Notes: + *

    + *
  • The test ignores cancellation from the {@code Subscriber} and emits the {@code onError} + * signal with a {@code null} parameter anyway.
  • + *
+ *

+ * If this test fails, the following could be checked within the {@code Subscriber} implementation: + *

    + *
  • if the {@code Subscriber} throws a {@code NullPointerException} from its {@code onNext} method + * in response to a {@code null} parameter and not some other unchecked exception or no exception at all. + *
+ */ + void required_spec213_blackbox_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; + /** + * Currently, this test is skipped because there is no agreed upon way for specifying, enforcing and testing + * a {@code Subscriber} with an arbitrary context. + *

+ * Verifies rule: 3.1 + */ + void untested_spec301_blackbox_mustNotBeCalledOutsideSubscriberContext() throws Exception; + /** + * Currently, this test is skipped because element production is the responsibility of the {@code Publisher} and + * a {@code Subscription} is not expected to be the active element in an established subscription. + *

+ * Verifies rule: 3.8 + */ + void untested_spec308_blackbox_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable; + /** + * Currently, this test is skipped because element production is the responsibility of the {@code Publisher} and + * a {@code Subscription} is not expected to be the active element in an established subscription. + *

+ * Verifies rule: 3.10 + *

+ * Notes: + *

    + *
  • This could be tested with a synchronous source currently not available within the TCK.
  • + *
+ */ + void untested_spec310_blackbox_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception; + /** + * Currently, this test is skipped because signal production is the responsibility of the {@code Publisher} and + * a {@code Subscription} is not expected to be the active element in an established subscription. + *

+ * Verifies rule: 3.11 + *

+ * Notes: + *

    + *
  • Tests {@link #required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() §2.9} + * and {@link #required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() §2.10} are + * supposed to cover this case from the {@code Subscriber's} perspective.
  • + *
+ */ + void untested_spec311_blackbox_requestMaySynchronouslyCallOnCompleteOrOnError() throws Exception; + /** + * Currently, this test is skipped because it is the responsibility of the {@code Publisher} deal with the case + * that all subscribers have cancelled their subscription. + *

+ * Verifies rule: 3.14 + *

+ * Notes: + *

    + *
  • The specification lists this as an optional behavior because only some {@code Publisher} implementations + * (most likely {@code Processor}s) would coordinate with multiple {@code Subscriber}s.
  • + *
+ */ + void untested_spec314_blackbox_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception; + /** + * Currently, this test is skipped because it requires more control over the {@code Subscriber} implementation + * thus there is no way to detect that the {@code Subscriber} called its own {@code onError} method in response + * to an exception thrown from {@code Subscription.cancel}. + *

+ * Verifies rule: 3.15 + */ + void untested_spec315_blackbox_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception; + /** + * Currently, this test is skipped because it requires more control over the {@code Subscriber} implementation + * thus there is no way to detect that the {@code Subscriber} called its own {@code onError} method in response + * to an exception thrown from {@code Subscription.request}. + *

+ * Verifies rule: 3.16 + */ + void untested_spec316_blackbox_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception; +} diff --git a/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBufferOverflowException.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBufferOverflowException.java new file mode 100644 index 00000000..9cb085d8 --- /dev/null +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBufferOverflowException.java @@ -0,0 +1,29 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +public final class SubscriberBufferOverflowException extends RuntimeException { + public SubscriberBufferOverflowException() { + } + + public SubscriberBufferOverflowException(String message) { + super(message); + } + + public SubscriberBufferOverflowException(String message, Throwable cause) { + super(message, cause); + } + + public SubscriberBufferOverflowException(Throwable cause) { + super(cause); + } +} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberWhiteboxVerificationRules.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberWhiteboxVerificationRules.java similarity index 68% rename from tck/src/main/java/org/reactivestreams/tck/support/SubscriberWhiteboxVerificationRules.java rename to tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberWhiteboxVerificationRules.java index 9368ca36..6c49c216 100644 --- a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberWhiteboxVerificationRules.java +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberWhiteboxVerificationRules.java @@ -1,4 +1,15 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; /** * Internal TCK use only. @@ -11,7 +22,7 @@ public interface SubscriberWhiteboxVerificationRules { void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable; void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable; void untested_spec204_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception; - void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception; + void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Throwable; void untested_spec206_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception; void untested_spec207_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception; void required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable; @@ -22,6 +33,9 @@ public interface SubscriberWhiteboxVerificationRules { void untested_spec211_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception; void untested_spec212_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality_specViolation() throws Throwable; void untested_spec213_failingOnSignalInvocation() throws Exception; + void required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; + void required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; + void required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; void untested_spec301_mustNotBeCalledOutsideSubscriberContext() throws Exception; void required_spec308_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable; void untested_spec310_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception; @@ -29,4 +43,4 @@ public interface SubscriberWhiteboxVerificationRules { void untested_spec314_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception; void untested_spec315_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception; void untested_spec316_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception; -} \ No newline at end of file +} diff --git a/tck/src/main/java/org/reactivestreams/tck/flow/support/TestException.java b/tck/src/main/java/org/reactivestreams/tck/flow/support/TestException.java new file mode 100644 index 00000000..17ac5cda --- /dev/null +++ b/tck/src/main/java/org/reactivestreams/tck/flow/support/TestException.java @@ -0,0 +1,22 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +/** + * Exception used by the TCK to signal failures. + * May be thrown or signalled through {@link org.reactivestreams.Subscriber#onError(Throwable)}. + */ +public final class TestException extends RuntimeException { + public TestException() { + super("Test Exception: Boom!"); + } +} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/Function.java b/tck/src/main/java/org/reactivestreams/tck/support/Function.java deleted file mode 100644 index d053e5c6..00000000 --- a/tck/src/main/java/org/reactivestreams/tck/support/Function.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.reactivestreams.tck.support; - -public interface Function { - public Out apply(In in) throws Throwable; -} \ No newline at end of file diff --git a/tck/src/main/java/org/reactivestreams/tck/support/PublisherVerificationRules.java b/tck/src/main/java/org/reactivestreams/tck/support/PublisherVerificationRules.java deleted file mode 100644 index 42c3ba7d..00000000 --- a/tck/src/main/java/org/reactivestreams/tck/support/PublisherVerificationRules.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.reactivestreams.tck.support; - - -/** - * Internal TCK use only. - * Add / Remove tests for PublisherVerification here to make sure that they arre added/removed in the other places. - */ -public interface PublisherVerificationRules { - void required_validate_maxElementsFromPublisher() throws Exception; - void required_validate_boundedDepthOfOnNextAndRequestRecursion() throws Exception; - void required_createPublisher1MustProduceAStreamOfExactly1Element() throws Throwable; - void required_createPublisher3MustProduceAStreamOfExactly3Elements() throws Throwable; - void required_spec101_subscriptionRequestMustResultInTheCorrectNumberOfProducedElements() throws Throwable; - void required_spec102_maySignalLessThanRequestedAndTerminateSubscription() throws Throwable; - void stochastic_spec103_mustSignalOnMethodsSequentially() throws Throwable; - void optional_spec104_mustSignalOnErrorWhenFails() throws Throwable; - void required_spec105_mustSignalOnCompleteWhenFiniteStreamTerminates() throws Throwable; - void optional_spec105_emptyStreamMustTerminateBySignallingOnComplete() throws Throwable; - void untested_spec106_mustConsiderSubscriptionCancelledAfterOnErrorOrOnCompleteHasBeenCalled() throws Throwable; - void required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSignalled() throws Throwable; - void untested_spec107_mustNotEmitFurtherSignalsOnceOnErrorHasBeenSignalled() throws Throwable; - void untested_spec108_possiblyCanceledSubscriptionShouldNotReceiveOnErrorOrOnCompleteSignals() throws Throwable; - void untested_spec109_subscribeShouldNotThrowNonFatalThrowable() throws Throwable; - void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice() throws Throwable; - void optional_spec111_maySupportMultiSubscribe() throws Throwable; - void required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe() throws Throwable; - void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingOneByOne() throws Throwable; - void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront() throws Throwable; - void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfrontAndCompleteAsExpected() throws Throwable; - void required_spec302_mustAllowSynchronousRequestCallsFromOnNextAndOnSubscribe() throws Throwable; - void required_spec303_mustNotAllowUnboundedRecursion() throws Throwable; - void untested_spec304_requestShouldNotPerformHeavyComputations() throws Exception; - void untested_spec305_cancelMustNotSynchronouslyPerformHeavyCompuatation() throws Exception; - void required_spec306_afterSubscriptionIsCancelledRequestMustBeNops() throws Throwable; - void required_spec307_afterSubscriptionIsCancelledAdditionalCancelationsMustBeNops() throws Throwable; - void required_spec309_requestZeroMustSignalIllegalArgumentException() throws Throwable; - void required_spec309_requestNegativeNumberMustSignalIllegalArgumentException() throws Throwable; - void required_spec312_cancelMustMakeThePublisherToEventuallyStopSignaling() throws Throwable; - void required_spec313_cancelMustMakeThePublisherEventuallyDropAllReferencesToTheSubscriber() throws Throwable; - void required_spec317_mustSupportAPendingElementCountUpToLongMaxValue() throws Throwable; - void required_spec317_mustSupportACumulativePendingElementCountUpToLongMaxValue() throws Throwable; - void required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue() throws Throwable; -} \ No newline at end of file diff --git a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBlackboxVerificationRules.java b/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBlackboxVerificationRules.java deleted file mode 100644 index b260957a..00000000 --- a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBlackboxVerificationRules.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.reactivestreams.tck.support; - -/** - * Internal TCK use only. - * Add / Remove tests for SubscriberBlackboxVerification here to make sure that they arre added/removed in the other places. - */ -public interface SubscriberBlackboxVerificationRules { - void required_spec201_blackbox_mustSignalDemandViaSubscriptionRequest() throws Throwable; - void untested_spec202_blackbox_shouldAsynchronouslyDispatch() throws Exception; - void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable; - void required_spec203_blackbox_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable; - void untested_spec204_blackbox_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception; - void required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Exception; - void untested_spec206_blackbox_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception; - void untested_spec207_blackbox_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception; - void untested_spec208_blackbox_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable; - void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable; - void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable; - void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable; - void untested_spec211_blackbox_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception; - void untested_spec212_blackbox_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality() throws Throwable; - void untested_spec213_blackbox_failingOnSignalInvocation() throws Exception; - void untested_spec301_blackbox_mustNotBeCalledOutsideSubscriberContext() throws Exception; - void required_spec308_blackbox_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable; - void untested_spec310_blackbox_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception; - void untested_spec311_blackbox_requestMaySynchronouslyCallOnCompleteOrOnError() throws Exception; - void untested_spec314_blackbox_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception; - void untested_spec315_blackbox_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception; - void untested_spec316_blackbox_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception; -} \ No newline at end of file diff --git a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBufferOverflowException.java b/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBufferOverflowException.java deleted file mode 100644 index c7150cfc..00000000 --- a/tck/src/main/java/org/reactivestreams/tck/support/SubscriberBufferOverflowException.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.reactivestreams.tck.support; - -public final class SubscriberBufferOverflowException extends RuntimeException { - public SubscriberBufferOverflowException() { - } - - public SubscriberBufferOverflowException(String message) { - super(message); - } - - public SubscriberBufferOverflowException(String message, Throwable cause) { - super(message, cause); - } - - public SubscriberBufferOverflowException(Throwable cause) { - super(cause); - } -} diff --git a/tck/src/main/java/org/reactivestreams/tck/support/TestException.java b/tck/src/main/java/org/reactivestreams/tck/support/TestException.java deleted file mode 100644 index 21e13ca3..00000000 --- a/tck/src/main/java/org/reactivestreams/tck/support/TestException.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.reactivestreams.tck.support; - -/** - * Exception used by the TCK to signal failures. - * May be thrown or signalled through {@link org.reactivestreams.Subscriber#onError(Throwable)}. - */ -public final class TestException extends RuntimeException { - public TestException() { - super("Test Exception: Boom!"); - } -} diff --git a/tck/src/test/java/org/reactivestreams/tck/EmptyLazyPublisherTest.java b/tck/src/test/java/org/reactivestreams/tck/EmptyLazyPublisherTest.java index 7332dbbd..1ae262b4 100644 --- a/tck/src/test/java/org/reactivestreams/tck/EmptyLazyPublisherTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/EmptyLazyPublisherTest.java @@ -1,3 +1,14 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.example.unicast.AsyncIterablePublisher; @@ -31,7 +42,7 @@ public Publisher createPublisher(long elements) { } @Override - public Publisher createErrorStatePublisher() { + public Publisher createFailedPublisher() { return null; } @@ -39,4 +50,4 @@ public Publisher createErrorStatePublisher() { public long maxElementsFromPublisher() { return 0; } -} \ No newline at end of file +} diff --git a/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationDelegationTest.java b/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationDelegationTest.java index c12c649f..ac0cd76c 100644 --- a/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationDelegationTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationDelegationTest.java @@ -1,12 +1,24 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; -import org.testng.AssertJUnit; import org.testng.annotations.Test; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import static org.testng.AssertJUnit.assertTrue; + /** * The {@link org.reactivestreams.tck.IdentityProcessorVerification} must also run all tests from * {@link org.reactivestreams.tck.PublisherVerification} and {@link org.reactivestreams.tck.SubscriberWhiteboxVerification}. @@ -52,7 +64,7 @@ private void assertSuiteDelegatedAllTests(Class delegatingFrom, List delegatingFrom, targetTest, targetClass.getSimpleName(), targetTest); - AssertJUnit.assertTrue(msg, testsInclude(allTests, targetTest)); + assertTrue(msg, testsInclude(allTests, targetTest)); } } diff --git a/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationTest.java b/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationTest.java index a6a02717..45701483 100644 --- a/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationTest.java @@ -1,10 +1,21 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.reactivestreams.tck.support.TCKVerificationSupport; +import org.reactivestreams.tck.flow.support.TCKVerificationSupport; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -19,6 +30,7 @@ public class IdentityProcessorVerificationTest extends TCKVerificationSupport { static final long DEFAULT_TIMEOUT_MILLIS = TestEnvironment.envDefaultTimeoutMillis(); + static final long DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS = TestEnvironment.envDefaultNoSignalsTimeoutMillis(); private ExecutorService ex; @BeforeClass void before() { ex = Executors.newFixedThreadPool(4); } @@ -41,7 +53,7 @@ public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANo return SKIP; } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } @@ -107,7 +119,7 @@ public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANo }; } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } }.required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError(); @@ -155,7 +167,7 @@ static class NoopProcessor implements Processor { } private TestEnvironment newTestEnvironment() { - return new TestEnvironment(DEFAULT_TIMEOUT_MILLIS); + return new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS); } diff --git a/tck/src/test/java/org/reactivestreams/tck/PublisherVerificationTest.java b/tck/src/test/java/org/reactivestreams/tck/PublisherVerificationTest.java index ba557cfe..ee4dc9b6 100644 --- a/tck/src/test/java/org/reactivestreams/tck/PublisherVerificationTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/PublisherVerificationTest.java @@ -1,19 +1,34 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.reactivestreams.tck.support.TCKVerificationSupport; -import org.reactivestreams.tck.support.TestException; +import org.reactivestreams.tck.flow.support.TCKVerificationSupport; +import org.reactivestreams.tck.flow.support.TestException; +import org.testng.Assert; import org.testng.annotations.Test; +import java.util.Collection; import java.util.Random; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; /** * Validates that the TCK's {@link org.reactivestreams.tck.PublisherVerification} fails with nice human readable errors. @@ -203,7 +218,7 @@ public void optional_spec105_emptyStreamMustTerminateBySignallingOnComplete_shou return 0; // it is an "empty" Publisher } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return null; } }; @@ -240,6 +255,7 @@ public void required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSigna // but keep signalling data if more demand comes in anyway! if (!completed) { s.onComplete(); + completed = true; } } @@ -251,51 +267,123 @@ public void required_spec107_mustNotEmitFurtherSignalsOnceOnCompleteHasBeenSigna } @Test - public void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice_shouldFailBy_skippingSinceOptional() throws Throwable { + public void required_spec109_subscribeThrowNPEOnNullSubscriber_shouldFailIfDoesntThrowNPE() throws Throwable { requireTestFailure(new ThrowingRunnable() { @Override public void run() throws Throwable { - noopPublisherVerification().untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice(); + customPublisherVerification(new Publisher() { + @Override public void subscribe(final Subscriber s) { + + } + }).required_spec109_subscribeThrowNPEOnNullSubscriber(); } - }, "Not verified by this TCK."); + }, "Publisher did not throw a NullPointerException when given a null Subscribe in subscribe"); } @Test - public void optional_spec111_maySupportMultiSubscribe_shouldFailBy_actuallyPass() throws Throwable { - noopPublisherVerification().optional_spec111_maySupportMultiSubscribe(); + public void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe_actuallyPass() throws Throwable { + customPublisherVerification(SKIP, new Publisher() { + @Override public void subscribe(Subscriber s) { + s.onSubscribe(new NoopSubscription()); + s.onError(new RuntimeException("Sorry, I'm busy now. Call me later.")); + } + }).required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe(); } @Test - public void optional_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe_shouldFail() throws Throwable { + public void required_spec109_mustIssueOnSubscribeForNonNullSubscriber_mustFailIfOnCompleteHappensFirst() throws Throwable { requireTestFailure(new ThrowingRunnable() { @Override public void run() throws Throwable { - customPublisherVerification(SKIP, new Publisher() { + customPublisherVerification(new Publisher() { + @Override + public void subscribe(Subscriber s) { + s.onComplete(); + } + }).required_spec109_mustIssueOnSubscribeForNonNullSubscriber(); + } + }, "onSubscribe should be called prior to onComplete always"); + } + + @Test + public void required_spec109_mustIssueOnSubscribeForNonNullSubscriber_mustFailIfOnNextHappensFirst() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + customPublisherVerification(new Publisher() { @Override public void subscribe(Subscriber s) { + s.onNext(1337); } - }).required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe(); + }).required_spec109_mustIssueOnSubscribeForNonNullSubscriber(); } - }, "Should have received onError"); + }, "onSubscribe should be called prior to onNext always"); } @Test - public void optional_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe_actuallyPass() throws Throwable { - customPublisherVerification(SKIP, new Publisher() { - @Override public void subscribe(Subscriber s) { - s.onError(new RuntimeException("Sorry, I'm busy now. Call me later.")); + public void required_spec109_mustIssueOnSubscribeForNonNullSubscriber_mustFailIfOnErrorHappensFirst() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + customPublisherVerification(new Publisher() { + @Override public void subscribe(Subscriber s) { + s.onError(new TestException()); + } + }).required_spec109_mustIssueOnSubscribeForNonNullSubscriber(); } - }).required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe(); + }, "onSubscribe should be called prior to onError always"); } @Test - public void required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe_beSkippedForNoGivenErrorPublisher() throws Throwable { + public void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe_shouldFail() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + customPublisherVerification(SKIP, new Publisher() { + @Override + public void subscribe(Subscriber s) { + s.onSubscribe(new NoopSubscription()); + } + }).required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe(); + } + }, "Should have received onError"); + } + + @Test + public void required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe_beSkippedForNoGivenErrorPublisher() throws Throwable { requireTestSkip(new ThrowingRunnable() { @Override public void run() throws Throwable { - noopPublisherVerification().required_spec112_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorInsteadOfOnSubscribe(); + noopPublisherVerification().required_spec109_mayRejectCallsToSubscribeIfPublisherIsUnableOrUnwillingToServeThemRejectionMustTriggerOnErrorAfterOnSubscribe(); } }, PublisherVerification.SKIPPING_NO_ERROR_PUBLISHER_AVAILABLE); } @Test - public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront_shouldFailBy_expectingOnError() throws Throwable { + public void untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice_shouldFailBy_skippingSinceOptional() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + noopPublisherVerification().untested_spec110_rejectASubscriptionRequestIfTheSameSubscriberSubscribesTwice(); + } + }, "Not verified by this TCK."); + } + + @Test + public void optional_spec111_maySupportMultiSubscribe_shouldFailBy_actuallyPass() throws Throwable { + noopPublisherVerification().optional_spec111_maySupportMultiSubscribe(); + } + + @Test + public void optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals_beSkippedWhenMultipleSubscribersNotSupported() throws Throwable { + requireTestSkip(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + multiSubscribersPublisherVerification(true).optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals(); + } + }, "Unexpected additional subscriber"); + } + + @Test + public void optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals_shouldPass() throws Throwable { + multiSubscribersPublisherVerification(false).optional_spec111_registeredSubscribersMustReceiveOnNextOrOnCompleteSignals(); + } + + @Test + public void optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront_shouldFailBy_expectingOnError() throws Throwable { requireTestFailure(new ThrowingRunnable() { @Override public void run() throws Throwable { customPublisherVerification(new Publisher() { @@ -313,7 +401,7 @@ public void required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfI } }); } - }).required_spec113_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront(); + }).optional_spec111_multicast_mustProduceTheSameElementsInTheSameSequenceToAllOfItsSubscribersWhenRequestingManyUpfront(); } }, "Expected elements to be signaled in the same sequence to 1st and 2nd subscribers: Lists differ at element "); } @@ -393,6 +481,27 @@ public void required_spec309_requestZeroMustSignalIllegalArgumentException_shoul }, "Expected onError"); } + @Test + public void required_spec309_requestZeroMustSignalIllegalArgumentException_shouldPass() throws Throwable { + customPublisherVerification(new Publisher() { + @Override + public void subscribe(final Subscriber s) { + s.onSubscribe(new Subscription() { + @Override + public void request(long n) { + // we error out with any message, it does not have to contain any specific wording + if (n <= 0) s.onError(new IllegalArgumentException("Illegal request value detected!")); + } + + @Override + public void cancel() { + // noop + } + }); + } + }).required_spec309_requestZeroMustSignalIllegalArgumentException(); + } + @Test public void required_spec309_requestNegativeNumberMustSignalIllegalArgumentException_shouldFailBy_expectingOnError() throws Throwable { requireTestFailure(new ThrowingRunnable() { @@ -528,6 +637,38 @@ public void required_spec317_mustSupportACumulativePendingElementCountUpToLongMa }, "Async error during test execution: Illegally signalling onError too soon!"); } + @Test + public void required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue_forSynchronousPublisher() throws Throwable { + final AtomicInteger sent = new AtomicInteger(); + + customPublisherVerification(new Publisher() { + @Override + public void subscribe(final Subscriber downstream) { + downstream.onSubscribe(new Subscription() { + boolean started; + boolean cancelled; + + @Override + public void request(long n) { + if (!started) { + started = true; + while (!cancelled) { + downstream.onNext(sent.getAndIncrement()); + } + } + } + + @Override + public void cancel() { + cancelled = true; + } + }); + } + }).required_spec317_mustNotSignalOnErrorWhenPendingAboveLongMaxValue(); + + // 11 due to the implementation of this particular TCK test (see impl) + Assert.assertEquals(sent.get(), 11); + } // FAILING IMPLEMENTATIONS // @@ -561,7 +702,7 @@ final PublisherVerification noopPublisherVerification() { } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } }; @@ -586,7 +727,7 @@ final PublisherVerification onErroringPublisherVerification() { } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } }; @@ -608,12 +749,79 @@ final PublisherVerification customPublisherVerification(final Publisher return pub; } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return errorPub; } }; } + /** + * Verification using a Publisher that supports multiple subscribers + * @param shouldBlowUp if true {@link RuntimeException} will be thrown during second subscription. + */ + final PublisherVerification multiSubscribersPublisherVerification(final boolean shouldBlowUp) { + return new PublisherVerification(newTestEnvironment()) { + + @Override + public Publisher createPublisher(final long elements) { + return new Publisher() { + + private final Collection subscriptions = new CopyOnWriteArrayList(); + private final AtomicLong source = new AtomicLong(elements); + + @Override + public void subscribe(Subscriber s) { + // onSubscribe first + CancelableSubscription subscription = new CancelableSubscription(s); + s.onSubscribe(subscription); + if (shouldBlowUp && !subscriptions.isEmpty()) { + s.onError(new RuntimeException("Unexpected additional subscriber")); + } else { + subscriptions.add(subscription); + } + } + + class CancelableSubscription implements Subscription { + + final AtomicBoolean canceled = new AtomicBoolean(); + Subscriber subscriber; + + CancelableSubscription(Subscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public void request(long n) { + if (!canceled.get()) { + for (long i = 0; i < n; i++) { + if (source.getAndDecrement() < 0) { + canceled.set(true); + subscriber.onComplete(); + } else { + subscriber.onNext((int) i); + } + } + } + } + + @Override + public void cancel() { + canceled.set(true); + subscriber = null; + subscriptions.remove(this); + } + } + + }; + } + + @Override + public Publisher createFailedPublisher() { + return SKIP; + } + }; + } + /** * Verification using a Publisher that publishes elements even with no demand available */ @@ -637,7 +845,7 @@ final PublisherVerification demandIgnoringSynchronousPublisherVerificat } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } }; @@ -725,7 +933,7 @@ final PublisherVerification demandIgnoringAsynchronousPublisherVerifica } - @Override public Publisher createErrorStatePublisher() { + @Override public Publisher createFailedPublisher() { return SKIP; } }; diff --git a/tck/src/test/java/org/reactivestreams/tck/RangePublisherTest.java b/tck/src/test/java/org/reactivestreams/tck/RangePublisherTest.java new file mode 100644 index 00000000..a76dee97 --- /dev/null +++ b/tck/src/test/java/org/reactivestreams/tck/RangePublisherTest.java @@ -0,0 +1,176 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.*; + +import org.reactivestreams.*; +import org.testng.annotations.*; + +@Test +public class RangePublisherTest extends PublisherVerification { + + static final Map stacks = new ConcurrentHashMap(); + + static final Map states = new ConcurrentHashMap(); + + static final AtomicInteger id = new AtomicInteger(); + + @AfterClass + public static void afterClass() { + boolean fail = false; + StringBuilder b = new StringBuilder(); + for (Map.Entry t : states.entrySet()) { + if (!t.getValue()) { + b.append("\r\n-------------------------------"); + for (Object o : stacks.get(t.getKey())) { + b.append("\r\nat ").append(o); + } + fail = true; + } + } + if (fail) { + throw new AssertionError("Cancellations were missing:" + b); + } + } + + public RangePublisherTest() { + super(new TestEnvironment()); + } + + @Override + public Publisher createPublisher(long elements) { + return new RangePublisher(1, elements); + } + + @Override + public Publisher createFailedPublisher() { + return null; + } + + static final class RangePublisher + implements Publisher { + + final StackTraceElement[] stacktrace; + + final long start; + + final long count; + + RangePublisher(long start, long count) { + this.stacktrace = Thread.currentThread().getStackTrace(); + this.start = start; + this.count = count; + } + + @Override + public void subscribe(Subscriber s) { + if (s == null) { + throw new NullPointerException(); + } + + int ids = id.incrementAndGet(); + + RangeSubscription parent = new RangeSubscription(s, ids, start, start + count); + stacks.put(ids, stacktrace); + states.put(ids, false); + s.onSubscribe(parent); + } + + static final class RangeSubscription extends AtomicLong implements Subscription { + + private static final long serialVersionUID = 9066221863682220604L; + + final Subscriber actual; + + final int ids; + + final long end; + + long index; + + volatile boolean cancelled; + + RangeSubscription(Subscriber actual, int ids, long start, long end) { + this.actual = actual; + this.ids = ids; + this.index = start; + this.end = end; + } + + @Override + public void request(long n) { + if (!cancelled) { + if (n <= 0L) { + cancelled = true; + states.put(ids, true); + actual.onError(new IllegalArgumentException("§3.9 violated")); + return; + } + + for (;;) { + long r = get(); + long u = r + n; + if (u < 0L) { + u = Long.MAX_VALUE; + } + if (compareAndSet(r, u)) { + if (r == 0) { + break; + } + return; + } + } + + long idx = index; + long f = end; + + for (;;) { + long e = 0; + while (e != n && idx != f) { + if (cancelled) { + return; + } + + actual.onNext((int)idx); + + idx++; + e++; + } + + if (idx == f) { + if (!cancelled) { + states.put(ids, true); + actual.onComplete(); + } + return; + } + + index = idx; + n = addAndGet(-n); + if (n == 0) { + break; + } + } + } + } + + @Override + public void cancel() { + cancelled = true; + states.put(ids, true); + } + } + } +} diff --git a/tck/src/test/java/org/reactivestreams/tck/SingleElementPublisherTest.java b/tck/src/test/java/org/reactivestreams/tck/SingleElementPublisherTest.java index 57abe94e..946b96c1 100644 --- a/tck/src/test/java/org/reactivestreams/tck/SingleElementPublisherTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/SingleElementPublisherTest.java @@ -1,6 +1,19 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.reactivestreams.example.unicast.AsyncIterablePublisher; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -31,7 +44,7 @@ public Publisher createPublisher(long elements) { } @Override - public Publisher createErrorStatePublisher() { + public Publisher createFailedPublisher() { return null; } @@ -39,4 +52,4 @@ public Publisher createErrorStatePublisher() { public long maxElementsFromPublisher() { return 1; } -} \ No newline at end of file +} diff --git a/tck/src/test/java/org/reactivestreams/tck/SubscriberBlackboxVerificationTest.java b/tck/src/test/java/org/reactivestreams/tck/SubscriberBlackboxVerificationTest.java index aab1979f..9f5055bf 100644 --- a/tck/src/test/java/org/reactivestreams/tck/SubscriberBlackboxVerificationTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/SubscriberBlackboxVerificationTest.java @@ -1,14 +1,27 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.reactivestreams.tck.support.TCKVerificationSupport; +import org.reactivestreams.tck.flow.support.TCKVerificationSupport; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; /** * Validates that the TCK's {@link org.reactivestreams.tck.SubscriberBlackboxVerification} fails with nice human readable errors. @@ -100,6 +113,34 @@ public void required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAn }, "illegally called `subscription.request(1)"); } + @Test + public void required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal_shouldGetCompletion() throws Throwable { + final CountDownLatch completion = new CountDownLatch(1); + + customSubscriberVerification(new KeepSubscriptionSubscriber() { + volatile Subscription sub; + + @Override + public void onSubscribe(Subscription s) { + super.onSubscribe(s); + if (sub != null) { + sub = s; + s.request(1); + } else { + // the second one we cancel + s.cancel(); + } + } + + @Override + public void onComplete() { + completion.countDown(); + } + }).required_spec205_blackbox_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal(); + + completion.await(1, TimeUnit.SECONDS); + } + @Test public void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall_shouldFail() throws Throwable { requireTestFailure(new ThrowingRunnable() { @@ -108,7 +149,7 @@ public void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalW // don't even request() }).required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall(); } - }, "did not call `registerOnComplete()`"); + }, "Did not receive expected `request` call within"); } @Test @@ -119,11 +160,38 @@ public void required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalW } @Test - public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall_shouldFail() throws Throwable { + public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall_shouldPass_withRequestingSubscriber() throws Throwable { + customSubscriberVerification(new NoopSubscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); // request anything + } + }).required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall(); + } + + @Test + public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall_shouldFail_withNoopSubscriber() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override + public void run() throws Throwable { + customSubscriberVerification(new NoopSubscriber() { + // not requesting, so we can't test the "request followed by failure" scenario + }).required_spec209_blackbox_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall(); + } + }, "Did not receive expected `request` call within"); + } + + @Test + public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall_shouldFail_withThrowingInsideOnError() throws Throwable { requireTestFailure(new ThrowingRunnable() { @Override public void run() throws Throwable { customSubscriberVerification(new NoopSubscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(1); + } + @Override public void onError(Throwable t) { // this is wrong in many ways (incl. spec violation), but aims to simulate user code which "blows up" when handling the onError signal throw new RuntimeException("Wrong, don't do this!", t); // don't do this @@ -133,6 +201,39 @@ public void required_spec210_blackbox_mustBePreparedToReceiveAnOnErrorSignalWith }, "Test Exception: Boom!"); // checks that the expected exception was delivered to onError, we don't expect anyone to implement onError so weirdly } + @Test + public void required_spec213_blackbox_mustThrowNullPointerExceptionWhenParametersAreNull_mustFailOnIgnoredNull_onSubscribe() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + + customSubscriberVerification(new NoopSubscriber()) + .required_spec213_blackbox_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + }, "onSubscribe(null) did not throw NullPointerException"); + } + + @Test + public void required_spec213_blackbox_mustThrowNullPointerExceptionWhenParametersAreNull_mustFailOnIgnoredNull_onNext() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + + customSubscriberVerification(new NoopSubscriber()) + .required_spec213_blackbox_onNext_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + }, "onNext(null) did not throw NullPointerException"); + } + + @Test + public void required_spec213_blackbox_mustThrowNullPointerExceptionWhenParametersAreNull_mustFailOnIgnoredNull_onError() throws Throwable { + requireTestFailure(new ThrowingRunnable() { + @Override public void run() throws Throwable { + + customSubscriberVerification(new NoopSubscriber()) + .required_spec213_blackbox_onError_mustThrowNullPointerExceptionWhenParametersAreNull(); + } + }, "onError(null) did not throw NullPointerException"); + } + // FAILING IMPLEMENTATIONS // /** diff --git a/tck/src/test/java/org/reactivestreams/tck/SubscriberWhiteboxVerificationTest.java b/tck/src/test/java/org/reactivestreams/tck/SubscriberWhiteboxVerificationTest.java index fb7023f2..bd0737d7 100644 --- a/tck/src/test/java/org/reactivestreams/tck/SubscriberWhiteboxVerificationTest.java +++ b/tck/src/test/java/org/reactivestreams/tck/SubscriberWhiteboxVerificationTest.java @@ -1,11 +1,22 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + package org.reactivestreams.tck; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.reactivestreams.tck.SubscriberWhiteboxVerification.SubscriberPuppet; import org.reactivestreams.tck.SubscriberWhiteboxVerification.WhiteboxSubscriberProbe; -import org.reactivestreams.tck.support.Function; -import org.reactivestreams.tck.support.TCKVerificationSupport; +import org.reactivestreams.tck.flow.support.Function; +import org.reactivestreams.tck.flow.support.TCKVerificationSupport; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -162,7 +173,7 @@ public Subscriber apply(WhiteboxSubscriberProbe probe) throws } }).required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal(); } - }, "illegally accepted a second Subscription"); + }, "Expected 2nd Subscription given to subscriber to be cancelled"); } @Test diff --git a/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberTest.java b/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberTest.java new file mode 100644 index 00000000..88ba57e3 --- /dev/null +++ b/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberTest.java @@ -0,0 +1,55 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck; + +import org.reactivestreams.Subscriber; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.reactivestreams.tck.flow.support.SyncTriggeredDemandSubscriber; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Test // Must be here for TestNG to find and run this, do not remove +public class SyncTriggeredDemandSubscriberTest extends SubscriberBlackboxVerification { + + private ExecutorService e; + @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } + @AfterClass void after() { if (e != null) e.shutdown(); } + + public SyncTriggeredDemandSubscriberTest() { + super(new TestEnvironment()); + } + + @Override public void triggerRequest(final Subscriber subscriber) { + ((SyncTriggeredDemandSubscriber)subscriber).triggerDemand(1); + } + + @Override public Subscriber createSubscriber() { + return new SyncTriggeredDemandSubscriber() { + private long acc; + @Override protected long foreach(final Integer element) { + acc += element; + return 1; + } + + @Override public void onComplete() { + } + }; + } + + @Override public Integer createElement(int element) { + return element; + } +} diff --git a/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberWhiteboxTest.java b/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberWhiteboxTest.java new file mode 100644 index 00000000..d4c03fc9 --- /dev/null +++ b/tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberWhiteboxTest.java @@ -0,0 +1,85 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import org.reactivestreams.tck.flow.support.SyncTriggeredDemandSubscriber; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Test // Must be here for TestNG to find and run this, do not remove +public class SyncTriggeredDemandSubscriberWhiteboxTest extends SubscriberWhiteboxVerification { + + private ExecutorService e; + @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } + @AfterClass void after() { if (e != null) e.shutdown(); } + + public SyncTriggeredDemandSubscriberWhiteboxTest() { + super(new TestEnvironment()); + } + + @Override + public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { + return new SyncTriggeredDemandSubscriber() { + @Override + public void onSubscribe(final Subscription s) { + super.onSubscribe(s); + + probe.registerOnSubscribe(new SubscriberPuppet() { + @Override + public void triggerRequest(long elements) { + s.request(elements); + } + + @Override + public void signalCancel() { + s.cancel(); + } + }); + } + + @Override + public void onNext(Integer element) { + super.onNext(element); + probe.registerOnNext(element); + } + + @Override + public void onError(Throwable cause) { + super.onError(cause); + probe.registerOnError(cause); + } + + @Override + public void onComplete() { + super.onComplete(); + probe.registerOnComplete(); + } + + @Override + protected long foreach(Integer element) { + return 1; + } + }; + } + + @Override public Integer createElement(int element) { + return element; + } + +} diff --git a/tck/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandSubscriber.java b/tck/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandSubscriber.java new file mode 100644 index 00000000..0df6b3bf --- /dev/null +++ b/tck/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandSubscriber.java @@ -0,0 +1,134 @@ +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; + +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +/** + * SyncTriggeredDemandSubscriber is an implementation of Reactive Streams `Subscriber`, + * it runs synchronously (on the Publisher's thread) and requests demand triggered from + * "the outside" using its `triggerDemand` method and from "the inside" using the return + * value of its user-defined `whenNext` method which is invoked to process each element. + * + * NOTE: The code below uses a lot of try-catches to show the reader where exceptions can be expected, and where they are forbidden. + */ +public abstract class SyncTriggeredDemandSubscriber implements Subscriber { + private Subscription subscription; // Obeying rule 3.1, we make this private! + private boolean done = false; + + @Override public void onSubscribe(final Subscription s) { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` + if (s == null) throw null; + + if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully + try { + s.cancel(); // Cancel the additional subscription + } catch(final Throwable t) { + //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 + (new IllegalStateException(s + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); + } + } else { + // We have to assign it locally before we use it, if we want to be a synchronous `Subscriber` + // Because according to rule 3.10, the Subscription is allowed to call `onNext` synchronously from within `request` + subscription = s; + } + } + + /** + * Requests the provided number of elements from the `Subscription` of this `Subscriber`. + * NOTE: This makes no attempt at thread safety so only invoke it once from the outside to initiate the demand. + * @return `true` if successful and `false` if not (either due to no `Subscription` or due to exceptions thrown) + */ + public boolean triggerDemand(final long n) { + final Subscription s = subscription; + if (s == null) return false; + else { + try { + s.request(n); + } catch(final Throwable t) { + // Subscription.request is not allowed to throw according to rule 3.16 + (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); + return false; + } + return true; + } + } + + @Override public void onNext(final T element) { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` + if (element == null) throw null; + + if (!done) { // If we aren't already done + try { + final long need = foreach(element); + if (need > 0) triggerDemand(need); + else if (need == 0) {} + else { + done(); + } + } catch (final Throwable t) { + done(); + try { + onError(t); + } catch (final Throwable t2) { + //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 + (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); + } + } + } + } + } + + // Showcases a convenience method to idempotently marking the Subscriber as "done", so we don't want to process more elements + // herefor we also need to cancel our `Subscription`. + private void done() { + //On this line we could add a guard against `!done`, but since rule 3.7 says that `Subscription.cancel()` is idempotent, we don't need to. + done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) + try { + subscription.cancel(); // Cancel the subscription + } catch(final Throwable t) { + //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 + (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); + } + } + + // This method is left as an exercise to the reader/extension point + // Don't forget to call `triggerDemand` at the end if you are interested in more data, + // a return value of < 0 indicates that the subscription should be cancelled, + // a value of 0 indicates that there is no current need, + // a value of > 0 indicates the current need. + protected abstract long foreach(final T element); + + @Override public void onError(final Throwable t) { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); + } else { + // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` + if (t == null) throw null; + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } + } + + @Override public void onComplete() { + if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec + (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); + } else { + // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 + // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 + } + } +} diff --git a/tck/src/test/java/org/reactivestreams/tck/support/TCKVerificationSupport.java b/tck/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java similarity index 86% rename from tck/src/test/java/org/reactivestreams/tck/support/TCKVerificationSupport.java rename to tck/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java index 8aa396e5..5251d231 100644 --- a/tck/src/test/java/org/reactivestreams/tck/support/TCKVerificationSupport.java +++ b/tck/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java @@ -1,4 +1,15 @@ -package org.reactivestreams.tck.support; +/************************************************************************ + * Licensed under Public Domain (CC0) * + * * + * To the extent possible under law, the person who associated CC0 with * + * this code has waived all copyright and related or neighboring * + * rights to this code. * + * * + * You should have received a copy of the CC0 legalcode along with this * + * work. If not, see .* + ************************************************************************/ + +package org.reactivestreams.tck.flow.support; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; diff --git a/tck/src/test/resources/testng.yaml b/tck/src/test/resources/testng.yaml index 854a0ba0..cb773852 100644 --- a/tck/src/test/resources/testng.yaml +++ b/tck/src/test/resources/testng.yaml @@ -6,5 +6,6 @@ tests: classes: - org.reactivestreams.tck.IdentityProcessorVerificationDelegationTest - org.reactivestreams.tck.PublisherVerificationTest + - org.reactivestreams.tck.BokenExampleTest - org.reactivestreams.tck.SubscriberBlackboxVerificationTest - org.reactivestreams.tck.SubscriberWhiteboxVerificationTest