├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .java-version ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE-NOTES.md ├── Relicensing.txt ├── api ├── .gitignore ├── build.gradle └── src │ └── main │ ├── java │ └── org │ │ └── reactivestreams │ │ ├── Processor.java │ │ ├── Publisher.java │ │ ├── Subscriber.java │ │ └── Subscription.java │ └── java9 │ └── org │ └── reactivestreams │ └── FlowAdapters.java ├── build.gradle ├── examples ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── reactivestreams │ │ └── example │ │ └── unicast │ │ ├── AsyncIterablePublisher.java │ │ ├── AsyncSubscriber.java │ │ ├── InfiniteIncrementNumberPublisher.java │ │ ├── NumberIterablePublisher.java │ │ ├── RangePublisher.java │ │ └── SyncSubscriber.java │ └── test │ └── java │ └── org │ └── reactivestreams │ └── example │ └── unicast │ ├── AsyncRangePublisherTest.java │ ├── AsyncSubscriberTest.java │ ├── IterablePublisherTest.java │ ├── RangePublisherTest.java │ ├── SyncSubscriberTest.java │ ├── SyncSubscriberWhiteboxTest.java │ └── UnboundedIntegerIncrementPublisherTest.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── tck-flow ├── README.md ├── build.gradle └── src │ ├── main │ └── java │ │ └── org │ │ └── reactivestreams │ │ └── tck │ │ └── flow │ │ ├── FlowPublisherVerification.java │ │ ├── FlowSubscriberBlackboxVerification.java │ │ ├── FlowSubscriberWhiteboxVerification.java │ │ └── IdentityFlowProcessorVerification.java │ └── test │ ├── java │ └── org │ │ └── reactivestreams │ │ ├── FlowAdaptersTest.java │ │ ├── MulticastPublisher.java │ │ ├── SubmissionPublisherTckTest.java │ │ ├── TestEitherConsumer.java │ │ └── tck │ │ └── flow │ │ ├── EmptyLazyFlowPublisherTest.java │ │ ├── LockstepFlowProcessorTest.java │ │ ├── RangeFlowPublisherTest.java │ │ ├── SingleElementFlowPublisherTest.java │ │ ├── SubmissionPublisherTest.java │ │ ├── SyncTriggeredDemandSubscriberTest.java │ │ ├── SyncTriggeredDemandSubscriberWhiteboxTest.java │ │ └── support │ │ └── SyncTriggeredDemandFlowSubscriber.java │ └── resources │ └── testng.yaml └── tck ├── README.md ├── build.gradle └── src ├── main └── java │ └── org │ └── reactivestreams │ └── tck │ ├── IdentityProcessorVerification.java │ ├── PublisherVerification.java │ ├── SubscriberBlackboxVerification.java │ ├── SubscriberWhiteboxVerification.java │ ├── TestEnvironment.java │ ├── WithHelperPublisher.java │ └── flow │ └── support │ ├── Function.java │ ├── HelperPublisher.java │ ├── InfiniteHelperPublisher.java │ ├── NonFatal.java │ ├── Optional.java │ ├── PublisherVerificationRules.java │ ├── SubscriberBlackboxVerificationRules.java │ ├── SubscriberBufferOverflowException.java │ ├── SubscriberWhiteboxVerificationRules.java │ └── TestException.java └── test ├── java └── org │ └── reactivestreams │ └── tck │ ├── EmptyLazyPublisherTest.java │ ├── IdentityProcessorVerificationDelegationTest.java │ ├── IdentityProcessorVerificationTest.java │ ├── LockstepProcessorTest.java │ ├── PublisherVerificationTest.java │ ├── RangePublisherTest.java │ ├── SingleElementPublisherTest.java │ ├── SubscriberBlackboxVerificationTest.java │ ├── SubscriberNoRegisterOnSubscribeTest.java │ ├── SubscriberWhiteboxVerificationTest.java │ ├── SyncTriggeredDemandSubscriberTest.java │ ├── SyncTriggeredDemandSubscriberWhiteboxTest.java │ └── flow │ └── support │ ├── SyncTriggeredDemandSubscriber.java │ └── TCKVerificationSupport.java └── resources └── testng.yaml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | java: [ '8', '11' ] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: ${{ matrix.java }} 23 | - name: print Java version 24 | run: java -version 25 | - name: Build with Gradle 26 | run: ./gradlew check 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .project 3 | .classpath 4 | .cache 5 | .target/ 6 | *~ 7 | .#* 8 | .*.swp 9 | .DS_Store 10 | .codefellow 11 | .ensime* 12 | .eprj 13 | .history 14 | .idea 15 | .idea_modules 16 | .gradle 17 | .settings 18 | bin 19 | build 20 | out 21 | *.iml 22 | *.ipr 23 | *.iws 24 | test-output 25 | test-results 26 | test-tmp 27 | *.class 28 | gradle.properties 29 | *.orig 30 | -------------------------------------------------------------------------------- /.java-version: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the Reactive Streams Project 2 | 3 | The Reactive Streams project welcomes contributions from anybody who wants to participate in moving this initiative forward. All code or documentation that is contributed will have to be covered by the **MIT No Attribution** (SPDX: MIT-0) license, the rationale for this is that the APIs defined by this project shall be freely implementable and usable by everyone. For more detail, see [LICENSE](https://github.com/reactive-streams/reactive-streams-jvm/blob/master/LICENSE). 4 | 5 | ## Gatekeepers 6 | 7 | To ensure consistent development of Reactive Streams towards their goal, a group of gatekeepers is defined: 8 | 9 | * Kaazing Corp., currently represented by Todd Montgomery (@tmontgomery) 10 | * Netflix Inc., currently represented by Ben Christensen (@benjchristensen) 11 | * Pivotal Software Inc., currently represented by Jon Brisbin (@jbrisbin) and Stéphane Maldini (@smaldini) 12 | * Red Hat Inc., currently represented by Tim Fox (@purplefox) and Norman Maurer (@normanmaurer) 13 | * Twitter Inc., currently represented by Marius Eriksen (@mariusaeriksen) 14 | * Typesafe Inc., currently represented by Viktor Klang (@viktorklang) and Roland Kuhn (@rkuhn) 15 | 16 | The role of this group is detailed in the following, additions to this list are made by pull request as defined below, removals require the consent of the entity to be removed or unanimous consent of all other Gatekeepers. Changing a representative of one of the gatekeeper entities can be done by a member of that entity without requiring consent from the other Gatekeepers. 17 | 18 | Gatekeepers commit to the following: 19 | 20 | 1. 1-week SLA on :+1: or :-1: Pull Requests 21 | * If a Gatekeeper will be unavailable for a period of time, notify @reactive-streams/contributors and appoint who will vote in his/her place in the mean time 22 | 2. tag @reactive-streams/contributors with a deadline when there needs to be a vote on an Issue, 23 | with at least 1 week of notice (see rule 1 above) 24 | 25 | ## General Workflow 26 | 27 | 1. Before starting to work on a change, make sure that: 28 | 1. There is a ticket for your work in the project's issue tracker. If not, create it first. It can help accelerating the pull request acceptance process if the change is agreed upon beforehand within the ticket, but in some cases it may be preferable to discuss the necessity of the change in consideration of a concrete proposal. 29 | 2. The ticket has been scheduled for the current milestone. 30 | 2. You should always perform your work in a Git feature branch within your own fork of the repository your are targeting (even if you should have push rights to the target repository). 31 | 3. When the change is completed you should open a [Pull Request](https://help.github.com/articles/using-pull-requests) on GitHub. 32 | 4. Anyone can comment on the pull request while it is open, and you are expected to answer questions or incorporate feedback. 33 | 5. Once at least two thirds of the gatekeepers have signaled their consent, the pull request is merged by one of the gatekeepers into the branch and repository it is targeting. Consent is signaled by commenting on the pull request with the text “LGTM”, and it suffices for one representative of a gatekeeper to signal consent for that gatekeeper to be counted towards the two thirds quorum. 34 | 6. It is not allowed to force-push to the branch on which the pull request is based. Replacing or adding to the commits on that branch will invalidate all previous consenting comments and consent needs to be re-established. 35 | 7. Before merging a branch that contains multiple commits, it is recommended that these are squashed into a single commit before performing the merge. To aid in verification that no new changes are introduced, a new pull request should be opened in this case, targeting the same branch and repository and containing just one commit which encompasses the full change set of the original pull request. 36 | 37 | ## Pull Request Requirements 38 | 39 | For a Pull Request to be considered at all it has to meet these requirements: 40 | 41 | 1. If applicable, the new or fixed features must be accompanied by comprehensive tests. 42 | 2. If applicable, the pull request must contain all necessary documentation updates required by the changes introduced. 43 | 3. The pull request must not contain changes that are unrelated to the ticket that it corresponds to. One pull request is meant to introduce only one logically contiguous change. 44 | 45 | ## Creating Commits And Writing Commit Messages 46 | 47 | Follow these guidelines when creating public commits and writing commit messages. 48 | 49 | 1. If your work spans multiple local commits (for example; if you do safe point commits while working in a feature branch or work in a branch for long time doing merges/rebases etc.) then please do not commit it all but rewrite the history by squashing the commits into a single big commit which you write a good commit message for (like discussed in the following sections). For more info read this article: [Git Workflow](http://sandofsky.com/blog/git-workflow.html). Every commit should be able to be used in isolation, cherry picked etc. 50 | 51 | 2. First line should be a descriptive sentence what the commit is doing. It should be possible to fully understand what the commit does—but not necessarily how it does it—by just reading this single line. We follow the “imperative present tense” style for commit messages ([more info here](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)). 52 | 53 | It is **not ok** to only list the ticket number, type "minor fix" or similar. In order to help with automatic filtering of the commit history (generating ChangeLogs, writing the migration guide, code archaeology) we use the following encoding: 54 | 55 | 3. Following the single line description should be a blank line followed by an enumerated list with the details of the commit. For very simple commits this may be empty. 56 | 57 | 4. Add keywords for your commit: 58 | * ``Review by @gituser`` - if you want to notify someone specifically for review; this has no influence on the acceptance process described above 59 | 60 | Example: 61 | 62 | add CONTRIBUTING.md 63 | 64 | * clarify how pull requests should look like 65 | * describe the acceptance process 66 | 67 | ## Performing Official Releases 68 | 69 | Creating binary artifacts, uploading them to central repositories and declaring these to be an official release of the Reactive Streams project requires the consent of all gatekeepers. The process is initiated by creating a ticket in the `reactive-streams` repository for this purpose and consent is signaled in the same way as for pull requests. The actual work of updating version numbers and publishing the artifacts will typically involve pull requests targeting the affected repositories. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT No Attribution 2 | 3 | Copyright 2014 Reactive Streams 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Relicensing.txt: -------------------------------------------------------------------------------- 1 | The following copyright holders agree that all of their contributions originally submitted to this project under Creative Commons 2 | Zero 1.0 Universal (SPDX: CC0-1.0) license are hereby relicensed to MIT No Attribution (SPDX: MIT-0) license, and are submitted pursuant to the Developer Certificate of Origin, version 1.1: 3 | 4 | 5 | github name | Real Name, Email Address used for git commits, Company 6 | ---------------+---------------------------------------------------------------------------- 7 | viktorklang | Viktor Klang, viktor.klang@gmail.com, Lightbend Inc. 8 | savulchik | Stanislav Savulchik, s.savulchik@gmail.com 9 | akarnokd | David Karnok, akarnokd@gmail.com 10 | OlegDokuka | Oleh Dokuka, oleh.dokuka@icloud.com, 11 | 2m | Martynas Mickevičius, self@2m.lt, Argyle Inc. 12 | rkuhn | Dr. Roland Kuhn, dr.roland.kuhn@gmail.com, Actyx AG 13 | anthonyvdotbe | Anthony Vanelverdinghe, dev@anthonyv.be 14 | angelsanz | Ángel Sanz, angelsanz@users.noreply.github.com 15 | shenghaiyang | 盛海洋, shenghaiyang@aliyun.com 16 | colinrgodsey | Colin Godsey, crgodsey@gmail.com 17 | patriknw | Patrik Nordwall, patrik.nordwall@gmail.com, Lightbend Inc 18 | retronym | Jason Zaugg, jzaugg@gmail.com, Lightbend Inc. 19 | ldaley | Luke Daley, luke@gradle.com, Gradle Inc. 20 | davidmoten | Dave Moten, davidmoten@gmail.com 21 | tomislavhofman | Tomislav Hofman, tomislav.hofman@gmail.com 22 | rstoyanchev | Rossen Stoyanchev, rstoyanchev@vmware.com, VMware 23 | ouertani | Slim Ouertani, ouertani@gmail.com 24 | egetman | Evgeniy Getman, getman.eugene@gmail.com 25 | ktoso | Konrad Malawski, konrad.malawski@project13.pl 26 | sullis | Sean Sullivan, github@seansullivan.com 27 | jroper | James Roper, james@jazzy.id.au, Lightbend Inc. 28 | smaldini | Stephane Maldini, smaldini@netflix.com, Netflix 29 | dl | Doug Lea dl@cs.oswego.edu 30 | benjchristensen| Ben Christensen, benjchristensen@gmail.com 31 | bjornhamels | Björn Hamels, bjorn@hamels.nl 32 | Scottmitch | Scott Mitchell, scott_mitchell@apple.com 33 | seratch | Kazuhiro Sera, seratch@gmail.com 34 | briantopping | Brian Topping, brian.topping@gmail.com, Mauswerks LLC 35 | JakeWharton | Jake Wharton, jakewharton@gmail.com, Square Inc. 36 | kiiadi | Kyle Thomson, kylthoms@amazon.com, Amazon.com 37 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | description = "reactive-streams" 2 | def jdkFlow = false 3 | try { 4 | Class.forName("java.util.concurrent.Flow") 5 | jdkFlow = true 6 | } catch (ClassNotFoundException cnfe) { 7 | 8 | } 9 | 10 | sourceSets { 11 | main { 12 | java { 13 | if (jdkFlow) 14 | srcDirs = ['src/main/java', 'src/main/java9'] 15 | else 16 | srcDirs = ['src/main/java'] 17 | } 18 | } 19 | } 20 | 21 | jar { 22 | bnd ('Bundle-Name': 'reactive-streams-jvm', 23 | 'Bundle-Vendor': 'Reactive Streams SIG', 24 | 'Bundle-Description': 'Reactive Streams API', 25 | 'Bundle-DocURL': 'http://reactive-streams.org', 26 | 'Bundle-Version': project.version, 27 | 'Export-Package': 'org.reactivestreams.*', 28 | 'Automatic-Module-Name': 'org.reactivestreams', 29 | 'Bundle-SymbolicName': 'org.reactivestreams.reactive-streams' 30 | ) 31 | } 32 | 33 | -------------------------------------------------------------------------------- /api/src/main/java/org/reactivestreams/Processor.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | /** 8 | * A {@link Processor} represents a processing stage—which is both a {@link Subscriber} 9 | * and a {@link Publisher} and obeys the contracts of both. 10 | * 11 | * @param the type of element signaled to the {@link Subscriber} 12 | * @param the type of element signaled by the {@link Publisher} 13 | */ 14 | public interface Processor extends Subscriber, Publisher { 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/org/reactivestreams/Publisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | /** 8 | * A {@link Publisher} is a provider of a potentially unbounded number of sequenced elements, publishing them according to 9 | * the demand received from its {@link Subscriber}(s). 10 | *

11 | * A {@link Publisher} can serve multiple {@link Subscriber}s subscribed {@link Publisher#subscribe(Subscriber)} dynamically 12 | * at various points in time. 13 | * 14 | * @param the type of element signaled 15 | */ 16 | public interface Publisher { 17 | 18 | /** 19 | * Request {@link Publisher} to start streaming data. 20 | *

21 | * This is a "factory method" and can be called multiple times, each time starting a new {@link Subscription}. 22 | *

23 | * Each {@link Subscription} will work for only a single {@link Subscriber}. 24 | *

25 | * A {@link Subscriber} should only subscribe once to a single {@link Publisher}. 26 | *

27 | * If the {@link Publisher} rejects the subscription attempt or otherwise fails it will 28 | * signal the error via {@link Subscriber#onError(Throwable)}. 29 | * 30 | * @param s the {@link Subscriber} that will consume signals from this {@link Publisher} 31 | */ 32 | public void subscribe(Subscriber s); 33 | } 34 | -------------------------------------------------------------------------------- /api/src/main/java/org/reactivestreams/Subscriber.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | /** 8 | * Will receive call to {@link #onSubscribe(Subscription)} once after passing an instance of {@link Subscriber} to {@link Publisher#subscribe(Subscriber)}. 9 | *

10 | * No further notifications will be received until {@link Subscription#request(long)} is called. 11 | *

12 | * After signaling demand: 13 | *

    14 | *
  • One or more invocations of {@link #onNext(Object)} up to the maximum number defined by {@link Subscription#request(long)}
  • 15 | *
  • Single invocation of {@link #onError(Throwable)} or {@link Subscriber#onComplete()} which signals a terminal state after which no further events will be sent. 16 | *
17 | *

18 | * Demand can be signaled via {@link Subscription#request(long)} whenever the {@link Subscriber} instance is capable of handling more. 19 | * 20 | * @param the type of element signaled 21 | */ 22 | public interface Subscriber { 23 | 24 | /** 25 | * Invoked after calling {@link Publisher#subscribe(Subscriber)}. 26 | *

27 | * No data will start flowing until {@link Subscription#request(long)} is invoked. 28 | *

29 | * It is the responsibility of this {@link Subscriber} instance to call {@link Subscription#request(long)} whenever more data is wanted. 30 | *

31 | * The {@link Publisher} will send notifications only in response to {@link Subscription#request(long)}. 32 | * 33 | * @param s the {@link Subscription} that allows requesting data via {@link Subscription#request(long)} 34 | */ 35 | public void onSubscribe(Subscription s); 36 | 37 | /** 38 | * Data notification sent by the {@link Publisher} in response to requests to {@link Subscription#request(long)}. 39 | * 40 | * @param t the element signaled 41 | */ 42 | public void onNext(T t); 43 | 44 | /** 45 | * Failed terminal state. 46 | *

47 | * No further events will be sent even if {@link Subscription#request(long)} is invoked again. 48 | * 49 | * @param t the throwable signaled 50 | */ 51 | public void onError(Throwable t); 52 | 53 | /** 54 | * Successful terminal state. 55 | *

56 | * No further events will be sent even if {@link Subscription#request(long)} is invoked again. 57 | */ 58 | public void onComplete(); 59 | } 60 | -------------------------------------------------------------------------------- /api/src/main/java/org/reactivestreams/Subscription.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | /** 8 | * A {@link Subscription} represents a one-to-one lifecycle of a {@link Subscriber} subscribing to a {@link Publisher}. 9 | *

10 | * It can only be used once by a single {@link Subscriber}. 11 | *

12 | * It is used to both signal desire for data and cancel demand (and allow resource cleanup). 13 | */ 14 | public interface Subscription { 15 | 16 | /** 17 | * No events will be sent by a {@link Publisher} until demand is signaled via this method. 18 | *

19 | * It can be called however often and whenever needed—but if the outstanding cumulative demand ever becomes Long.MAX_VALUE or more, 20 | * it may be treated by the {@link Publisher} as "effectively unbounded". 21 | *

22 | * Whatever has been requested can be sent by the {@link Publisher} so only signal demand for what can be safely handled. 23 | *

24 | * A {@link Publisher} can send less than is requested if the stream ends but 25 | * then must emit either {@link Subscriber#onError(Throwable)} or {@link Subscriber#onComplete()}. 26 | * 27 | * @param n the strictly positive number of elements to requests to the upstream {@link Publisher} 28 | */ 29 | public void request(long n); 30 | 31 | /** 32 | * Request the {@link Publisher} to stop sending data and clean up resources. 33 | *

34 | * Data may still be sent to meet previously signalled demand after calling cancel. 35 | */ 36 | public void cancel(); 37 | } 38 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | maven { 6 | url "https://plugins.gradle.org/m2/" 7 | } 8 | } 9 | dependencies { 10 | classpath "biz.aQute.bnd:biz.aQute.bnd.gradle:4.3.1" 11 | } 12 | } 13 | 14 | subprojects { 15 | apply plugin: "java-library" 16 | apply plugin: 'biz.aQute.bnd.builder' 17 | 18 | group = "org.reactivestreams" 19 | version = "1.0.4" 20 | 21 | sourceCompatibility = 1.6 22 | targetCompatibility = 1.6 23 | 24 | tasks.withType(JavaCompile) { 25 | configure(options) { 26 | compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 27 | encoding = "UTF-8" 28 | } 29 | } 30 | 31 | tasks.withType(Javadoc) { 32 | configure(options) { 33 | encoding "UTF-8" 34 | docEncoding "UTF-8" 35 | charSet "UTF-8" 36 | linkSource true 37 | noTimestamp true 38 | } 39 | } 40 | 41 | tasks.withType(Test) { 42 | testLogging { 43 | exceptionFormat "full" 44 | events "failed", "started", "standard_out", "standard_error" 45 | } 46 | } 47 | 48 | repositories { 49 | mavenCentral() 50 | } 51 | 52 | if (name in ["reactive-streams", 53 | "reactive-streams-tck", 54 | "reactive-streams-tck-flow", 55 | "reactive-streams-examples"]) { 56 | apply plugin: "maven" 57 | apply plugin: "signing" 58 | apply plugin: "maven-publish" 59 | 60 | signing { 61 | sign configurations.archives 62 | required { gradle.taskGraph.hasTask(uploadArchives) } 63 | } 64 | task sourcesJar(type: Jar) { 65 | classifier "sources" 66 | from sourceSets.main.allSource 67 | } 68 | 69 | task javadocJar(type: Jar) { 70 | classifier "javadoc" 71 | from javadoc 72 | } 73 | 74 | publishing { 75 | publications { 76 | mavenJava(MavenPublication) { 77 | from components.java 78 | } 79 | } 80 | repositories { 81 | mavenLocal() 82 | } 83 | } 84 | 85 | artifacts { 86 | archives sourcesJar, javadocJar 87 | } 88 | 89 | uploadArchives { 90 | repositories { 91 | mavenDeployer { 92 | gradle.taskGraph.whenReady { taskGraph -> 93 | if (taskGraph.hasTask(uploadArchives)) { 94 | def userProp = "sonatypeOssUsername" 95 | def passwordProp = "sonatypeOssPassword" 96 | def user = project.properties[userProp] 97 | def password = project.properties[passwordProp] 98 | 99 | if (user == null || password == null) { 100 | throw new InvalidUserDataException( 101 | "Cannot perform $uploadArchives.path due to missing credentials.\n" + 102 | "Run with command line args `-P$userProp=«username» -P$passwordProp=«password»` or add these properties to $gradle.gradleUserHomeDir/gradle.properties.\n") 103 | } 104 | 105 | repository(url: "https://oss.sonatype.org/service/local/staging/deploy/maven2/") { 106 | authentication(userName: user, password: password) 107 | } 108 | snapshotRepository(url: "https://oss.sonatype.org/content/repositories/snapshots/") { 109 | authentication(userName: user, password: password) 110 | } 111 | } 112 | } 113 | } 114 | } 115 | } 116 | 117 | tasks.withType(Upload) { 118 | repositories.withType(MavenResolver) { 119 | it.beforeDeployment { signing.signPom(it) } 120 | it.pom.whenConfigured { pom -> 121 | pom.project { 122 | url "http://www.reactive-streams.org/" 123 | name "reactive-streams" 124 | description "A Protocol for Asynchronous Non-Blocking Data Sequence" 125 | inceptionYear "2014" 126 | 127 | scm { 128 | url "git@github.com:reactive-streams/reactive-streams.git" 129 | connection "scm:git:git@github.com:reactive-streams/reactive-streams.git" 130 | } 131 | 132 | licenses { 133 | license { 134 | name "MIT-0" 135 | url "https://spdx.org/licenses/MIT-0.html" 136 | distribution "repo" 137 | } 138 | } 139 | 140 | developers { 141 | developer { 142 | id "reactive-streams-sig" 143 | name "Reactive Streams SIG" 144 | url "http://www.reactive-streams.org/" 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } else { 152 | uploadArchives.enabled = false 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /examples/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'reactive-streams-examples' 2 | dependencies { 3 | implementation project(':reactive-streams') 4 | testImplementation project(':reactive-streams-tck') 5 | } 6 | 7 | jar { 8 | bnd ('Bundle-Name': 'reactive-streams-jvm', 9 | 'Bundle-Vendor': 'Reactive Streams SIG', 10 | 'Bundle-Description': 'Reactive Streams Examples', 11 | 'Bundle-DocURL': 'http://reactive-streams.org', 12 | 'Bundle-Version': project.version, 13 | 'Export-Package': 'org.reactivestreams.example.*', 14 | 'Automatic-Module-Name': 'org.reactivestreams.examples', 15 | 'Bundle-SymbolicName': 'org.reactivestreams.examples' 16 | ) 17 | } 18 | 19 | test.useTestNG() 20 | -------------------------------------------------------------------------------- /examples/src/main/java/org/reactivestreams/example/unicast/InfiniteIncrementNumberPublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import java.util.Iterator; 8 | import java.util.concurrent.Executor; 9 | 10 | import org.reactivestreams.Subscription; 11 | import org.reactivestreams.Subscriber; 12 | import org.reactivestreams.Publisher; 13 | 14 | public class InfiniteIncrementNumberPublisher extends AsyncIterablePublisher { 15 | public InfiniteIncrementNumberPublisher(final Executor executor) { 16 | super(new Iterable() { 17 | @Override public Iterator iterator() { 18 | return new Iterator() { 19 | private int at = 0; 20 | @Override public boolean hasNext() { return true; } 21 | @Override public Integer next() { return at++; } // Wraps around on overflow 22 | @Override public void remove() { throw new UnsupportedOperationException(); } 23 | }; 24 | } 25 | }, executor); 26 | } 27 | } -------------------------------------------------------------------------------- /examples/src/main/java/org/reactivestreams/example/unicast/NumberIterablePublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import java.util.Collections; 8 | import java.util.Iterator; 9 | import java.util.concurrent.Executor; 10 | import org.reactivestreams.Subscription; 11 | import org.reactivestreams.Subscriber; 12 | import org.reactivestreams.Publisher; 13 | 14 | public class NumberIterablePublisher extends AsyncIterablePublisher { 15 | public NumberIterablePublisher(final int from, final int to, final Executor executor) { 16 | super(new Iterable() { 17 | { if(from > to) throw new IllegalArgumentException("from must be equal or greater than to!"); } 18 | @Override public Iterator iterator() { 19 | return new Iterator() { 20 | private int at = from; 21 | @Override public boolean hasNext() { return at < to; } 22 | @Override public Integer next() { 23 | if (!hasNext()) return Collections.emptyList().iterator().next(); 24 | else return at++; 25 | } 26 | @Override public void remove() { throw new UnsupportedOperationException(); } 27 | }; 28 | } 29 | }, executor); 30 | } 31 | } -------------------------------------------------------------------------------- /examples/src/main/java/org/reactivestreams/example/unicast/RangePublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.*; 8 | 9 | import java.util.concurrent.atomic.AtomicLong; 10 | 11 | /** 12 | * A synchronous implementation of the {@link Publisher} that can 13 | * be subscribed to multiple times and each individual subscription 14 | * will receive range of monotonically increasing integer values on demand. 15 | */ 16 | public final class RangePublisher implements Publisher { 17 | 18 | /** The starting value of the range. */ 19 | final int start; 20 | 21 | /** The number of items to emit. */ 22 | final int count; 23 | 24 | /** 25 | * Constructs a RangePublisher instance with the given start and count values 26 | * that yields a sequence of [start, start + count). 27 | * @param start the starting value of the range 28 | * @param count the number of items to emit 29 | */ 30 | public RangePublisher(int start, int count) { 31 | this.start = start; 32 | this.count = count; 33 | } 34 | 35 | @Override 36 | public void subscribe(Subscriber subscriber) { 37 | // As per rule 1.11, we have decided to support multiple subscribers 38 | // in a unicast configuration for this `Publisher` implementation. 39 | 40 | // As per rule 1.09, we need to throw a `java.lang.NullPointerException` 41 | // if the `Subscriber` is `null` 42 | if (subscriber == null) throw null; 43 | 44 | // As per 2.13, this method must return normally (i.e. not throw). 45 | try { 46 | subscriber.onSubscribe(new RangeSubscription(subscriber, start, start + count)); 47 | } catch (Throwable ex) { 48 | new IllegalStateException(subscriber + " violated the Reactive Streams rule 2.13 " + 49 | "by throwing an exception from onSubscribe.", ex) 50 | // When onSubscribe fails this way, we don't know what state the 51 | // subscriber is thus calling onError may cause more crashes. 52 | .printStackTrace(); 53 | } 54 | } 55 | 56 | /** 57 | * A Subscription implementation that holds the current downstream 58 | * requested amount and responds to the downstream's request() and 59 | * cancel() calls. 60 | */ 61 | static final class RangeSubscription 62 | // We are using this `AtomicLong` to make sure that this `Subscription` 63 | // doesn't run concurrently with itself, which would violate rule 1.3 64 | // among others (no concurrent notifications). 65 | // The atomic transition from 0L to N > 0L will ensure this. 66 | extends AtomicLong implements Subscription { 67 | 68 | private static final long serialVersionUID = -9000845542177067735L; 69 | 70 | /** The Subscriber we are emitting integer values to. */ 71 | final Subscriber downstream; 72 | 73 | /** The end index (exclusive). */ 74 | final int end; 75 | 76 | /** 77 | * The current index and within the [start, start + count) range that 78 | * will be emitted as downstream.onNext(). 79 | */ 80 | int index; 81 | 82 | /** 83 | * Indicates the emission should stop. 84 | */ 85 | volatile boolean cancelled; 86 | 87 | /** 88 | * Holds onto the IllegalArgumentException (containing the offending stacktrace) 89 | * indicating there was a non-positive request() call from the downstream. 90 | */ 91 | volatile Throwable invalidRequest; 92 | 93 | /** 94 | * Constructs a stateful RangeSubscription that emits signals to the given 95 | * downstream from an integer range of [start, end). 96 | * @param downstream the Subscriber receiving the integer values and the completion signal. 97 | * @param start the first integer value emitted, start of the range 98 | * @param end the end of the range, exclusive 99 | */ 100 | RangeSubscription(Subscriber downstream, int start, int end) { 101 | this.downstream = downstream; 102 | this.index = start; 103 | this.end = end; 104 | } 105 | 106 | // This method will register inbound demand from our `Subscriber` and 107 | // validate it against rule 3.9 and rule 3.17 108 | @Override 109 | public void request(long n) { 110 | // Non-positive requests should be honored with IllegalArgumentException 111 | if (n <= 0L) { 112 | invalidRequest = new IllegalArgumentException("§3.9: non-positive requests are not allowed!"); 113 | n = 1; 114 | } 115 | // Downstream requests are cumulative and may come from any thread 116 | for (;;) { 117 | long requested = get(); 118 | long update = requested + n; 119 | // As governed by rule 3.17, when demand overflows `Long.MAX_VALUE` 120 | // we treat the signalled demand as "effectively unbounded" 121 | if (update < 0L) { 122 | update = Long.MAX_VALUE; 123 | } 124 | // atomically update the current requested amount 125 | if (compareAndSet(requested, update)) { 126 | // if there was no prior request amount, we start the emission loop 127 | if (requested == 0L) { 128 | emit(update); 129 | } 130 | break; 131 | } 132 | } 133 | } 134 | 135 | // This handles cancellation requests, and is idempotent, thread-safe and not 136 | // synchronously performing heavy computations as specified in rule 3.5 137 | @Override 138 | public void cancel() { 139 | // Indicate to the emission loop it should stop. 140 | cancelled = true; 141 | } 142 | 143 | void emit(long currentRequested) { 144 | // Load fields to avoid re-reading them from memory due to volatile accesses in the loop. 145 | Subscriber downstream = this.downstream; 146 | int index = this.index; 147 | int end = this.end; 148 | int emitted = 0; 149 | 150 | try { 151 | for (; ; ) { 152 | // Check if there was an invalid request and then report its exception 153 | // as mandated by rule 3.9. The stacktrace in it should 154 | // help locate the faulty logic in the Subscriber. 155 | Throwable invalidRequest = this.invalidRequest; 156 | if (invalidRequest != null) { 157 | // When we signal onError, the subscription must be considered as cancelled, as per rule 1.6 158 | cancelled = true; 159 | 160 | downstream.onError(invalidRequest); 161 | return; 162 | } 163 | 164 | // Loop while the index hasn't reached the end and we haven't 165 | // emitted all that's been requested 166 | while (index != end && emitted != currentRequested) { 167 | // to make sure that we follow rule 1.8, 3.6 and 3.7 168 | // We stop if cancellation was requested. 169 | if (cancelled) { 170 | return; 171 | } 172 | 173 | downstream.onNext(index); 174 | 175 | // Increment the index for the next possible emission. 176 | index++; 177 | // Increment the emitted count to prevent overflowing the downstream. 178 | emitted++; 179 | } 180 | 181 | // If the index reached the end, we complete the downstream. 182 | if (index == end) { 183 | // to make sure that we follow rule 1.8, 3.6 and 3.7 184 | // Unless cancellation was requested by the last onNext. 185 | if (!cancelled) { 186 | // We need to consider this `Subscription` as cancelled as per rule 1.6 187 | // Note, however, that this state is not observable from the outside 188 | // world and since we leave the loop with requested > 0L, any 189 | // further request() will never trigger the loop. 190 | cancelled = true; 191 | 192 | downstream.onComplete(); 193 | } 194 | return; 195 | } 196 | 197 | // Did the requested amount change while we were looping? 198 | long freshRequested = get(); 199 | if (freshRequested == currentRequested) { 200 | // Save where the loop has left off: the next value to be emitted 201 | this.index = index; 202 | // Atomically subtract the previously requested (also emitted) amount 203 | currentRequested = addAndGet(-currentRequested); 204 | // If there was no new request in between get() and addAndGet(), we simply quit 205 | // The next 0 to N transition in request() will trigger the next emission loop. 206 | if (currentRequested == 0L) { 207 | break; 208 | } 209 | // Looks like there were more async requests, reset the emitted count and continue. 210 | emitted = 0; 211 | } else { 212 | // Yes, avoid the atomic subtraction and resume. 213 | // emitted != currentRequest in this case and index 214 | // still points to the next value to be emitted 215 | currentRequested = freshRequested; 216 | } 217 | } 218 | } catch (Throwable ex) { 219 | // We can only get here if `onNext`, `onError` or `onComplete` threw, and they 220 | // are not allowed to according to 2.13, so we can only cancel and log here. 221 | // If `onError` throws an exception, this is a spec violation according to rule 1.9, 222 | // and all we can do is to log it. 223 | 224 | // Make sure that we are cancelled, since we cannot do anything else 225 | // since the `Subscriber` is faulty. 226 | cancelled = true; 227 | 228 | // We can't report the failure to onError as the Subscriber is unreliable. 229 | (new IllegalStateException(downstream + " violated the Reactive Streams rule 2.13 by " + 230 | "throwing an exception from onNext, onError or onComplete.", ex)) 231 | .printStackTrace(); 232 | } 233 | } 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /examples/src/main/java/org/reactivestreams/example/unicast/SyncSubscriber.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | 10 | /** 11 | * SyncSubscriber is an implementation of Reactive Streams `Subscriber`, 12 | * it runs synchronously (on the Publisher's thread) and requests one element 13 | * at a time and invokes a user-defined method to process each element. 14 | * 15 | * NOTE: The code below uses a lot of try-catches to show the reader where exceptions can be expected, and where they are forbidden. 16 | */ 17 | public abstract class SyncSubscriber implements Subscriber { 18 | private Subscription subscription; // Obeying rule 3.1, we make this private! 19 | private boolean done = false; 20 | 21 | @Override public void onSubscribe(final Subscription s) { 22 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` 23 | if (s == null) throw null; 24 | 25 | if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully 26 | try { 27 | s.cancel(); // Cancel the additional subscription 28 | } catch(final Throwable t) { 29 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 30 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 31 | } 32 | } else { 33 | // We have to assign it locally before we use it, if we want to be a synchronous `Subscriber` 34 | // Because according to rule 3.10, the Subscription is allowed to call `onNext` synchronously from within `request` 35 | subscription = s; 36 | try { 37 | // If we want elements, according to rule 2.1 we need to call `request` 38 | // And, according to rule 3.2 we are allowed to call this synchronously from within the `onSubscribe` method 39 | s.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time 40 | } catch(final Throwable t) { 41 | // Subscription.request is not allowed to throw according to rule 3.16 42 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); 43 | } 44 | } 45 | } 46 | 47 | @Override public void onNext(final T element) { 48 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 49 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); 50 | } else { 51 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` 52 | if (element == null) throw null; 53 | 54 | if (!done) { // If we aren't already done 55 | try { 56 | if (whenNext(element)) { 57 | try { 58 | subscription.request(1); // Our Subscriber is unbuffered and modest, it requests one element at a time 59 | } catch (final Throwable t) { 60 | // Subscription.request is not allowed to throw according to rule 3.16 61 | (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); 62 | } 63 | } else { 64 | done(); 65 | } 66 | } catch (final Throwable t) { 67 | done(); 68 | try { 69 | onError(t); 70 | } catch (final Throwable t2) { 71 | //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 72 | (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | // Showcases a convenience method to idempotently marking the Subscriber as "done", so we don't want to process more elements 80 | // herefor we also need to cancel our `Subscription`. 81 | private void done() { 82 | //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. 83 | done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) 84 | try { 85 | subscription.cancel(); // Cancel the subscription 86 | } catch(final Throwable t) { 87 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 88 | (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 89 | } 90 | } 91 | 92 | // This method is left as an exercise to the reader/extension point 93 | // Returns whether more elements are desired or not, and if no more elements are desired 94 | protected abstract boolean whenNext(final T element); 95 | 96 | @Override public void onError(final Throwable t) { 97 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 98 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); 99 | } else { 100 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` 101 | if (t == null) throw null; 102 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 103 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 104 | } 105 | } 106 | 107 | @Override public void onComplete() { 108 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 109 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); 110 | } else { 111 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 112 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/AsyncRangePublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | package org.reactivestreams.example.unicast; 5 | 6 | import org.reactivestreams.Publisher; 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | import org.reactivestreams.tck.PublisherVerification; 10 | import org.reactivestreams.tck.TestEnvironment; 11 | import org.testng.annotations.AfterClass; 12 | import org.testng.annotations.BeforeClass; 13 | import org.testng.annotations.Test; 14 | 15 | import java.util.concurrent.BlockingQueue; 16 | import java.util.concurrent.Executor; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import java.util.concurrent.LinkedBlockingQueue; 20 | 21 | @Test // Must be here for TestNG to find and run this, do not remove 22 | public class AsyncRangePublisherTest extends PublisherVerification { 23 | private static final int TERMINAL_DELAY_MS = 20; 24 | private static final int DEFAULT_TIMEOUT_MS = 5000; 25 | private static final int DEFAULT_POLL_INTERVAL_MS = TERMINAL_DELAY_MS / 2; 26 | private ExecutorService e; 27 | @BeforeClass 28 | void before() { e = Executors.newCachedThreadPool(); } 29 | @AfterClass 30 | void after() { if (e != null) e.shutdown(); } 31 | 32 | public AsyncRangePublisherTest() { 33 | super(new TestEnvironment(DEFAULT_TIMEOUT_MS, 50, DEFAULT_POLL_INTERVAL_MS)); 34 | } 35 | 36 | @Override 37 | public Publisher createPublisher(long elements) { 38 | return new AsyncPublisher(new RangePublisher(1, (int)elements), e); 39 | } 40 | 41 | @Override 42 | public Publisher createFailedPublisher() { 43 | return null; 44 | } 45 | 46 | private static final class AsyncPublisher implements Publisher { 47 | private final Publisher original; 48 | private final Executor executor; 49 | 50 | private AsyncPublisher(Publisher original, Executor executor) { 51 | this.original = requireNonNull(original); 52 | this.executor = requireNonNull(executor); 53 | } 54 | 55 | @Override 56 | public void subscribe(Subscriber s) { 57 | AsyncSubscriber.wrapAndSubscribe(original, requireNonNull(s), executor); 58 | } 59 | 60 | private static final class AsyncSubscriber implements Subscriber { 61 | private final BlockingQueue signalQueue = new LinkedBlockingQueue(); 62 | 63 | static void wrapAndSubscribe(final Publisher publisher, 64 | final Subscriber targetSubscriber, final Executor executor) { 65 | final AsyncSubscriber asyncSubscriber = new AsyncSubscriber(); 66 | try { 67 | executor.execute(new Runnable() { 68 | private Subscription subscription; 69 | private boolean terminated; 70 | @Override 71 | public void run() { 72 | try { 73 | for (; ; ) { 74 | final Object signal = asyncSubscriber.signalQueue.take(); 75 | if (signal instanceof Cancelled) { 76 | return; 77 | } else if (signal instanceof TerminalSignal) { 78 | // sleep intentional to verify TestEnvironment.expectError behavior. 79 | Thread.sleep(TERMINAL_DELAY_MS); 80 | 81 | TerminalSignal terminalSignal = (TerminalSignal) signal; 82 | terminated = true; 83 | if (terminalSignal.cause == null) { 84 | targetSubscriber.onComplete(); 85 | } else { 86 | targetSubscriber.onError(terminalSignal.cause); 87 | } 88 | return; 89 | } else if (signal instanceof OnSubscribeSignal) { 90 | // We distribute the subscription downstream and may also call cancel on this 91 | // thread if an exception is thrown. Since there is no concurrency allowed, make 92 | // the subscription safe for concurrency. 93 | subscription = concurrentSafe(((OnSubscribeSignal) signal).subscription); 94 | targetSubscriber.onSubscribe(subscription); 95 | } else { 96 | @SuppressWarnings("unchecked") final T onNextSignal = ((OnNextSignal) signal).onNext; 97 | targetSubscriber.onNext(onNextSignal); 98 | } 99 | } 100 | } catch (Throwable cause) { 101 | if (!terminated) { 102 | try { 103 | if (subscription == null) { 104 | targetSubscriber.onSubscribe(noopSubscription()); 105 | } else { 106 | subscription.cancel(); 107 | } 108 | } finally { 109 | terminated = true; 110 | targetSubscriber.onError(new IllegalStateException("run loop interrupted", cause)); 111 | } 112 | } 113 | } 114 | } 115 | }); 116 | } catch (Throwable cause) { 117 | try { 118 | targetSubscriber.onSubscribe(noopSubscription()); 119 | } finally { 120 | targetSubscriber.onError(new IllegalStateException("Executor rejected", cause)); 121 | } 122 | // Publisher rejected the target subscriber and terminated it, don't continue to subscribe to avoid 123 | // duplicate termination. 124 | return; 125 | } 126 | publisher.subscribe(asyncSubscriber); 127 | } 128 | 129 | @Override 130 | public void onSubscribe(final Subscription s) { 131 | signalQueue.add(new OnSubscribeSignal(new Subscription() { 132 | @Override 133 | public void request(long n) { 134 | s.request(n); 135 | } 136 | 137 | @Override 138 | public void cancel() { 139 | try { 140 | s.cancel(); 141 | } finally { 142 | signalQueue.add(new Cancelled()); 143 | } 144 | } 145 | })); 146 | } 147 | 148 | @Override 149 | public void onNext(T t) { 150 | signalQueue.add(new OnNextSignal(t)); 151 | } 152 | 153 | @Override 154 | public void onError(Throwable t) { 155 | signalQueue.add(new TerminalSignal(requireNonNull(t))); 156 | } 157 | 158 | @Override 159 | public void onComplete() { 160 | signalQueue.add(new TerminalSignal(null)); 161 | } 162 | } 163 | 164 | private static Subscription concurrentSafe(final Subscription subscription) { 165 | // TODO: make concurrent safe. TCK interacts from the subscription concurrently so lock isn't sufficient. 166 | return subscription; 167 | } 168 | 169 | private static Subscription noopSubscription() { 170 | return new Subscription() { 171 | @Override 172 | public void request(long n) { 173 | } 174 | 175 | @Override 176 | public void cancel() { 177 | } 178 | }; 179 | } 180 | 181 | private static final class TerminalSignal { 182 | private final Throwable cause; 183 | 184 | private TerminalSignal(Throwable cause) { 185 | this.cause = cause; 186 | } 187 | } 188 | 189 | private static final class OnSubscribeSignal { 190 | private final Subscription subscription; 191 | 192 | private OnSubscribeSignal(Subscription subscription) { 193 | this.subscription = subscription; 194 | } 195 | } 196 | 197 | private static final class OnNextSignal { 198 | private final T onNext; 199 | 200 | private OnNextSignal(T onNext) { 201 | this.onNext = onNext; 202 | } 203 | } 204 | 205 | private static final class Cancelled { 206 | } 207 | 208 | private static T requireNonNull(T o) { 209 | if (o == null) { 210 | throw new NullPointerException(); 211 | } 212 | return o; 213 | } 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/AsyncSubscriberTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 10 | import org.reactivestreams.tck.TestEnvironment; 11 | import org.testng.annotations.Test; 12 | import static org.testng.Assert.assertEquals; 13 | 14 | import org.testng.annotations.BeforeClass; 15 | import org.testng.annotations.AfterClass; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.CountDownLatch; 19 | import java.util.concurrent.atomic.AtomicLong; 20 | import java.util.concurrent.TimeUnit; 21 | 22 | @Test // Must be here for TestNG to find and run this, do not remove 23 | public class AsyncSubscriberTest extends SubscriberBlackboxVerification { 24 | 25 | private ExecutorService e; 26 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 27 | @AfterClass void after() { if (e != null) e.shutdown(); } 28 | 29 | public AsyncSubscriberTest() { 30 | super(new TestEnvironment()); 31 | } 32 | 33 | @Override public Subscriber createSubscriber() { 34 | return new AsyncSubscriber(e) { 35 | @Override protected boolean whenNext(final Integer element) { 36 | return true; 37 | } 38 | }; 39 | } 40 | 41 | @Test public void testAccumulation() throws InterruptedException { 42 | 43 | final AtomicLong i = new AtomicLong(Long.MIN_VALUE); 44 | final CountDownLatch latch = new CountDownLatch(1); 45 | final Subscriber sub = new AsyncSubscriber(e) { 46 | private long acc; 47 | @Override protected boolean whenNext(final Integer element) { 48 | acc += element; 49 | return true; 50 | } 51 | 52 | @Override protected void whenComplete() { 53 | i.set(acc); 54 | latch.countDown(); 55 | } 56 | }; 57 | 58 | new NumberIterablePublisher(0, 10, e).subscribe(sub); 59 | latch.await(env.defaultTimeoutMillis() * 10, TimeUnit.MILLISECONDS); 60 | assertEquals(i.get(), 45); 61 | } 62 | 63 | @Override public Integer createElement(int element) { 64 | return element; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/IterablePublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import java.lang.Override; 8 | import java.util.Collections; 9 | import java.util.Iterator; 10 | import org.reactivestreams.Publisher; 11 | import org.reactivestreams.tck.PublisherVerification; 12 | import org.reactivestreams.tck.TestEnvironment; 13 | import org.testng.annotations.Test; 14 | 15 | import org.testng.annotations.BeforeClass; 16 | import org.testng.annotations.AfterClass; 17 | import java.util.concurrent.Executors; 18 | import java.util.concurrent.ExecutorService; 19 | 20 | @Test // Must be here for TestNG to find and run this, do not remove 21 | public class IterablePublisherTest extends PublisherVerification { 22 | 23 | private ExecutorService e; 24 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 25 | @AfterClass void after() { if (e != null) e.shutdown(); } 26 | 27 | public IterablePublisherTest() { 28 | super(new TestEnvironment()); 29 | } 30 | 31 | @SuppressWarnings("unchecked") 32 | @Override public Publisher createPublisher(final long elements) { 33 | assert(elements <= maxElementsFromPublisher()); 34 | return new NumberIterablePublisher(0, (int)elements, e); 35 | } 36 | 37 | @Override public Publisher createFailedPublisher() { 38 | return new AsyncIterablePublisher(new Iterable() { 39 | @Override public Iterator iterator() { 40 | throw new RuntimeException("Error state signal!"); 41 | } 42 | }, e); 43 | } 44 | 45 | @Override public long maxElementsFromPublisher() { 46 | return Integer.MAX_VALUE; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/RangePublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.example.unicast.RangePublisher; 9 | import org.reactivestreams.tck.*; 10 | 11 | public class RangePublisherTest extends PublisherVerification { 12 | public RangePublisherTest() { 13 | super(new TestEnvironment(50, 50)); 14 | } 15 | 16 | @Override 17 | public Publisher createPublisher(long elements) { 18 | return new RangePublisher(1, (int)elements); 19 | } 20 | 21 | @Override 22 | public Publisher createFailedPublisher() { 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.testng.annotations.AfterClass; 11 | import org.testng.annotations.BeforeClass; 12 | import org.testng.annotations.Test; 13 | 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | @Test // Must be here for TestNG to find and run this, do not remove 18 | public class SyncSubscriberTest extends SubscriberBlackboxVerification { 19 | 20 | private ExecutorService e; 21 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 22 | @AfterClass void after() { if (e != null) e.shutdown(); } 23 | 24 | public SyncSubscriberTest() { 25 | super(new TestEnvironment()); 26 | } 27 | 28 | @Override public Subscriber createSubscriber() { 29 | return new SyncSubscriber() { 30 | private long acc; 31 | @Override protected boolean whenNext(final Integer element) { 32 | acc += element; 33 | return true; 34 | } 35 | 36 | @Override public void onComplete() { 37 | System.out.println("Accumulated: " + acc); 38 | } 39 | }; 40 | } 41 | 42 | @Override public Integer createElement(int element) { 43 | return element; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/SyncSubscriberWhiteboxTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 10 | import org.reactivestreams.tck.SubscriberWhiteboxVerification; 11 | import org.reactivestreams.tck.TestEnvironment; 12 | import org.testng.annotations.AfterClass; 13 | import org.testng.annotations.BeforeClass; 14 | import org.testng.annotations.Test; 15 | 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | @Test // Must be here for TestNG to find and run this, do not remove 20 | public class SyncSubscriberWhiteboxTest extends SubscriberWhiteboxVerification { 21 | 22 | private ExecutorService e; 23 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 24 | @AfterClass void after() { if (e != null) e.shutdown(); } 25 | 26 | public SyncSubscriberWhiteboxTest() { 27 | super(new TestEnvironment()); 28 | } 29 | 30 | @Override 31 | public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { 32 | return new SyncSubscriber() { 33 | @Override 34 | public void onSubscribe(final Subscription s) { 35 | super.onSubscribe(s); 36 | 37 | probe.registerOnSubscribe(new SubscriberPuppet() { 38 | @Override 39 | public void triggerRequest(long elements) { 40 | s.request(elements); 41 | } 42 | 43 | @Override 44 | public void signalCancel() { 45 | s.cancel(); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | public void onNext(Integer element) { 52 | super.onNext(element); 53 | probe.registerOnNext(element); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable cause) { 58 | super.onError(cause); 59 | probe.registerOnError(cause); 60 | } 61 | 62 | @Override 63 | public void onComplete() { 64 | super.onComplete(); 65 | probe.registerOnComplete(); 66 | } 67 | 68 | @Override 69 | protected boolean whenNext(Integer element) { 70 | return true; 71 | } 72 | }; 73 | } 74 | 75 | @Override public Integer createElement(int element) { 76 | return element; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /examples/src/test/java/org/reactivestreams/example/unicast/UnboundedIntegerIncrementPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.example.unicast; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.tck.PublisherVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.testng.annotations.Test; 11 | import org.testng.annotations.BeforeClass; 12 | import org.testng.annotations.AfterClass; 13 | import java.util.concurrent.Executors; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.Iterator; 16 | 17 | @Test // Must be here for TestNG to find and run this, do not remove 18 | public class UnboundedIntegerIncrementPublisherTest extends PublisherVerification { 19 | 20 | private ExecutorService e; 21 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 22 | @AfterClass void after() { if (e != null) e.shutdown(); } 23 | 24 | public UnboundedIntegerIncrementPublisherTest() { 25 | super(new TestEnvironment()); 26 | } 27 | 28 | @Override public Publisher createPublisher(long elements) { 29 | return new InfiniteIncrementNumberPublisher(e); 30 | } 31 | 32 | @Override public Publisher createFailedPublisher() { 33 | return new AsyncIterablePublisher(new Iterable() { 34 | @Override public Iterator iterator() { 35 | throw new RuntimeException("Error state signal!"); 36 | } 37 | }, e); 38 | } 39 | 40 | @Override public long maxElementsFromPublisher() { 41 | return super.publisherUnableToSignalOnComplete(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactive-streams/reactive-streams-jvm/a625d3aba756e9842ad1291a5b73f5db280b6168/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'reactive-streams' 2 | 3 | def jdkFlow = false 4 | 5 | final def ANSI_RESET = "\u001B[0m" 6 | final def ANSI_RED = "\u001B[31m" 7 | final def ANSI_GREEN = "\u001B[32m" 8 | final def ANSI_YELLOW = "\u001B[33m" 9 | 10 | try { 11 | Class.forName("java.util.concurrent.Flow") 12 | jdkFlow = true 13 | println(ANSI_GREEN + " INFO: ------------------ JDK9 classes detected ---------------------------------" + ANSI_RESET) 14 | println(ANSI_GREEN + " INFO: Java 9 Flow API found; Including [tck-flow] & FlowAdapters in build. " + ANSI_RESET) 15 | println(ANSI_GREEN + " INFO: --------------------------------------------------------------------------" + ANSI_RESET) 16 | } catch (Throwable ex) { 17 | // Flow API not available 18 | println(ANSI_RED + "WARNING: -------------------- JDK9 classes NOT detected -----------------------------" + ANSI_RESET) 19 | println(ANSI_RED + "WARNING: Java 9 Flow API not found; Not including [tck-flow] & FlowAdapters in build." + ANSI_RESET) 20 | println(ANSI_RED + "WARNING: In order to execute the complete test-suite run the build using JDK9+. " + ANSI_RESET) 21 | println(ANSI_RED + "WARNING: ----------------------------------------------------------------------------" + ANSI_RESET) 22 | } 23 | 24 | include ':reactive-streams' 25 | include ':reactive-streams-tck' 26 | include ':reactive-streams-examples' 27 | 28 | if (jdkFlow) { 29 | include ':reactive-streams-tck-flow' 30 | } 31 | 32 | project(':reactive-streams').projectDir = "$rootDir/api" as File 33 | project(':reactive-streams-tck').projectDir = "$rootDir/tck" as File 34 | project(':reactive-streams-examples').projectDir = "$rootDir/examples" as File 35 | if (jdkFlow) { 36 | project(':reactive-streams-tck-flow').projectDir = "$rootDir/tck-flow" as File 37 | } 38 | -------------------------------------------------------------------------------- /tck-flow/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'reactive-streams-tck-flow' 2 | dependencies { 3 | api project(':reactive-streams-tck') 4 | implementation project(':reactive-streams-examples') 5 | } 6 | 7 | jar { 8 | bnd ('Bundle-Name': 'reactive-streams-jvm', 9 | 'Bundle-Vendor': 'Reactive Streams SIG', 10 | 'Bundle-Description': 'Reactive Streams TCK Flow', 11 | 'Bundle-DocURL': 'http://reactive-streams.org', 12 | 'Bundle-Version': project.version, 13 | 'Export-Package': 'org.reactivestreams.tck.flow.*', 14 | 'Automatic-Module-Name': 'org.reactivestreams.tckflow', 15 | 'Bundle-SymbolicName': 'org.reactivestreams.tckflow' 16 | ) 17 | } 18 | 19 | test.useTestNG() 20 | -------------------------------------------------------------------------------- /tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowPublisherVerification.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.FlowAdapters; 9 | import org.reactivestreams.tck.PublisherVerification; 10 | import org.reactivestreams.tck.TestEnvironment; 11 | 12 | import java.util.concurrent.Flow; 13 | 14 | /** 15 | * Provides tests for verifying a Java 9+ {@link java.util.concurrent.Flow.Publisher} specification rules. 16 | * 17 | * @see java.util.concurrent.Flow.Publisher 18 | */ 19 | public abstract class FlowPublisherVerification extends PublisherVerification { 20 | 21 | public FlowPublisherVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis) { 22 | super(env, publisherReferenceGCTimeoutMillis); 23 | } 24 | 25 | public FlowPublisherVerification(TestEnvironment env) { 26 | super(env); 27 | } 28 | 29 | @Override 30 | final public Publisher createPublisher(long elements) { 31 | final Flow.Publisher flowPublisher = createFlowPublisher(elements); 32 | return FlowAdapters.toPublisher(flowPublisher); 33 | } 34 | /** 35 | * This is the main method you must implement in your test incarnation. 36 | * It must create a Publisher for a stream with exactly the given number of elements. 37 | * If `elements` is `Long.MAX_VALUE` the produced stream must be infinite. 38 | */ 39 | public abstract Flow.Publisher createFlowPublisher(long elements); 40 | 41 | @Override 42 | final public Publisher createFailedPublisher() { 43 | final Flow.Publisher failed = createFailedFlowPublisher(); 44 | if (failed == null) return null; // because `null` means "SKIP" in createFailedPublisher 45 | else return FlowAdapters.toPublisher(failed); 46 | } 47 | /** 48 | * By implementing this method, additional TCK tests concerning a "failed" publishers will be run. 49 | * 50 | * The expected behaviour of the {@link Flow.Publisher} returned by this method is hand out a subscription, 51 | * followed by signalling {@code onError} on it, as specified by Rule 1.9. 52 | * 53 | * If you ignore these additional tests, return {@code null} from this method. 54 | */ 55 | public abstract Flow.Publisher createFailedFlowPublisher(); 56 | } 57 | -------------------------------------------------------------------------------- /tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberBlackboxVerification.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.FlowAdapters; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | import org.reactivestreams.tck.SubscriberBlackboxVerification; 11 | import org.reactivestreams.tck.TestEnvironment; 12 | import org.reactivestreams.tck.flow.support.SubscriberBlackboxVerificationRules; 13 | 14 | import java.util.concurrent.Flow; 15 | 16 | /** 17 | * Provides tests for verifying {@link java.util.concurrent.Flow.Subscriber} and {@link java.util.concurrent.Flow.Subscription} 18 | * specification rules, without any modifications to the tested implementation (also known as "Black Box" testing). 19 | * 20 | * This verification is NOT able to check many of the rules of the spec, and if you want more 21 | * verification of your implementation you'll have to implement {@code org.reactivestreams.tck.SubscriberWhiteboxVerification} 22 | * instead. 23 | * 24 | * @see java.util.concurrent.Flow.Subscriber 25 | * @see java.util.concurrent.Flow.Subscription 26 | */ 27 | public abstract class FlowSubscriberBlackboxVerification extends SubscriberBlackboxVerification 28 | implements SubscriberBlackboxVerificationRules { 29 | 30 | protected FlowSubscriberBlackboxVerification(TestEnvironment env) { 31 | super(env); 32 | } 33 | 34 | @Override 35 | public final void triggerRequest(Subscriber subscriber) { 36 | triggerFlowRequest(FlowAdapters.toFlowSubscriber(subscriber)); 37 | } 38 | /** 39 | * Override this method if the {@link java.util.concurrent.Flow.Subscriber} implementation you are verifying 40 | * needs an external signal before it signals demand to its Publisher. 41 | * 42 | * By default this method does nothing. 43 | */ 44 | public void triggerFlowRequest(Flow.Subscriber subscriber) { 45 | // this method is intentionally left blank 46 | } 47 | 48 | @Override 49 | public final Subscriber createSubscriber() { 50 | return FlowAdapters.toSubscriber(createFlowSubscriber()); 51 | } 52 | /** 53 | * This is the main method you must implement in your test incarnation. 54 | * It must create a new {@link Flow.Subscriber} instance to be subjected to the testing logic. 55 | */ 56 | abstract public Flow.Subscriber createFlowSubscriber(); 57 | 58 | } 59 | -------------------------------------------------------------------------------- /tck-flow/src/main/java/org/reactivestreams/tck/flow/FlowSubscriberWhiteboxVerification.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | package org.reactivestreams.tck.flow; 5 | 6 | import org.reactivestreams.FlowAdapters; 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.tck.SubscriberWhiteboxVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; 11 | 12 | import java.util.concurrent.Flow; 13 | 14 | /** 15 | * Provides whitebox style tests for verifying {@link java.util.concurrent.Flow.Subscriber} 16 | * and {@link java.util.concurrent.Flow.Subscription} specification rules. 17 | * 18 | * @see java.util.concurrent.Flow.Subscriber 19 | * @see java.util.concurrent.Flow.Subscription 20 | */ 21 | public abstract class FlowSubscriberWhiteboxVerification extends SubscriberWhiteboxVerification 22 | implements SubscriberWhiteboxVerificationRules { 23 | 24 | protected FlowSubscriberWhiteboxVerification(TestEnvironment env) { 25 | super(env); 26 | } 27 | 28 | @Override 29 | final public Subscriber createSubscriber(WhiteboxSubscriberProbe probe) { 30 | return FlowAdapters.toSubscriber(createFlowSubscriber(probe)); 31 | } 32 | /** 33 | * This is the main method you must implement in your test incarnation. 34 | * It must create a new {@link org.reactivestreams.Subscriber} instance to be subjected to the testing logic. 35 | * 36 | * In order to be meaningfully testable your Subscriber must inform the given 37 | * `WhiteboxSubscriberProbe` of the respective events having been received. 38 | */ 39 | protected abstract Flow.Subscriber createFlowSubscriber(WhiteboxSubscriberProbe probe); 40 | } 41 | -------------------------------------------------------------------------------- /tck-flow/src/main/java/org/reactivestreams/tck/flow/IdentityFlowProcessorVerification.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.*; 8 | import org.reactivestreams.tck.IdentityProcessorVerification; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.reactivestreams.tck.flow.support.SubscriberWhiteboxVerificationRules; 11 | import org.reactivestreams.tck.flow.support.PublisherVerificationRules; 12 | 13 | import java.util.concurrent.Flow; 14 | 15 | public abstract class IdentityFlowProcessorVerification extends IdentityProcessorVerification 16 | implements SubscriberWhiteboxVerificationRules, PublisherVerificationRules { 17 | 18 | public IdentityFlowProcessorVerification(TestEnvironment env) { 19 | super(env); 20 | } 21 | 22 | public IdentityFlowProcessorVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis) { 23 | super(env, publisherReferenceGCTimeoutMillis); 24 | } 25 | 26 | public IdentityFlowProcessorVerification(TestEnvironment env, long publisherReferenceGCTimeoutMillis, int processorBufferSize) { 27 | super(env, publisherReferenceGCTimeoutMillis, processorBufferSize); 28 | } 29 | 30 | /** 31 | * By implementing this method, additional TCK tests concerning a "failed" Flow publishers will be run. 32 | * 33 | * The expected behaviour of the {@link Flow.Publisher} returned by this method is hand out a subscription, 34 | * followed by signalling {@code onError} on it, as specified by Rule 1.9. 35 | * 36 | * If you want to ignore these additional tests, return {@code null} from this method. 37 | */ 38 | protected abstract Flow.Publisher createFailedFlowPublisher(); 39 | 40 | /** 41 | * This is the main method you must implement in your test incarnation. 42 | * It must create a {@link Flow.Processor}, which simply forwards all stream elements from its upstream 43 | * to its downstream. It must be able to internally buffer the given number of elements. 44 | * 45 | * @param bufferSize number of elements the processor is required to be able to buffer. 46 | */ 47 | protected abstract Flow.Processor createIdentityFlowProcessor(int bufferSize); 48 | 49 | @Override 50 | public final Processor createIdentityProcessor(int bufferSize) { 51 | return FlowAdapters.toProcessor(createIdentityFlowProcessor(bufferSize)); 52 | } 53 | 54 | @Override 55 | public final Publisher createFailedPublisher() { 56 | Flow.Publisher failed = createFailedFlowPublisher(); 57 | if (failed == null) return null; // because `null` means "SKIP" in createFailedPublisher 58 | else return FlowAdapters.toPublisher(failed); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/FlowAdaptersTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | import org.testng.Assert; 8 | import org.testng.annotations.Test; 9 | 10 | import java.io.IOException; 11 | import java.util.concurrent.Executor; 12 | import java.util.concurrent.Flow; 13 | import java.util.concurrent.SubmissionPublisher; 14 | 15 | public class FlowAdaptersTest { 16 | @Test 17 | public void reactiveToFlowNormal() { 18 | MulticastPublisher p = new MulticastPublisher(new Executor() { 19 | @Override 20 | public void execute(Runnable command) { 21 | command.run(); 22 | } 23 | }, Flow.defaultBufferSize()); 24 | 25 | TestEitherConsumer tc = new TestEitherConsumer(); 26 | 27 | FlowAdapters.toFlowPublisher(p).subscribe(tc); 28 | 29 | p.offer(1); 30 | p.offer(2); 31 | p.offer(3); 32 | p.offer(4); 33 | p.offer(5); 34 | p.complete(); 35 | 36 | tc.assertRange(1, 5); 37 | } 38 | 39 | @Test 40 | public void reactiveToFlowError() { 41 | MulticastPublisher p = new MulticastPublisher(new Executor() { 42 | @Override 43 | public void execute(Runnable command) { 44 | command.run(); 45 | } 46 | }, Flow.defaultBufferSize()); 47 | 48 | TestEitherConsumer tc = new TestEitherConsumer(); 49 | 50 | FlowAdapters.toFlowPublisher(p).subscribe(tc); 51 | 52 | p.offer(1); 53 | p.offer(2); 54 | p.offer(3); 55 | p.offer(4); 56 | p.offer(5); 57 | p.completeExceptionally(new IOException()); 58 | 59 | tc.assertFailure(IOException.class, 1, 2, 3, 4, 5); 60 | } 61 | 62 | @Test 63 | public void flowToReactiveNormal() { 64 | SubmissionPublisher p = new SubmissionPublisher(new Executor() { 65 | @Override 66 | public void execute(Runnable command) { 67 | command.run(); 68 | } 69 | }, Flow.defaultBufferSize()); 70 | 71 | TestEitherConsumer tc = new TestEitherConsumer(); 72 | 73 | FlowAdapters.toPublisher(p).subscribe(tc); 74 | 75 | p.submit(1); 76 | p.submit(2); 77 | p.submit(3); 78 | p.submit(4); 79 | p.submit(5); 80 | p.close(); 81 | 82 | tc.assertRange(1, 5); 83 | } 84 | 85 | @Test 86 | public void flowToReactiveError() { 87 | SubmissionPublisher p = new SubmissionPublisher(new Executor() { 88 | @Override 89 | public void execute(Runnable command) { 90 | command.run(); 91 | } 92 | }, Flow.defaultBufferSize()); 93 | 94 | TestEitherConsumer tc = new TestEitherConsumer(); 95 | 96 | FlowAdapters.toPublisher(p).subscribe(tc); 97 | 98 | p.submit(1); 99 | p.submit(2); 100 | p.submit(3); 101 | p.submit(4); 102 | p.submit(5); 103 | p.closeExceptionally(new IOException()); 104 | 105 | tc.assertFailure(IOException.class, 1, 2, 3, 4, 5); 106 | } 107 | 108 | @Test 109 | public void reactiveStreamsToFlowSubscriber() { 110 | TestEitherConsumer tc = new TestEitherConsumer(); 111 | 112 | Flow.Subscriber fs = FlowAdapters.toFlowSubscriber(tc); 113 | 114 | final Object[] state = { null, null }; 115 | 116 | fs.onSubscribe(new Flow.Subscription() { 117 | @Override 118 | public void request(long n) { 119 | state[0] = n; 120 | } 121 | 122 | @Override 123 | public void cancel() { 124 | state[1] = true; 125 | } 126 | }); 127 | 128 | Assert.assertEquals(state[0], Long.MAX_VALUE); 129 | 130 | fs.onNext(1); 131 | fs.onNext(2); 132 | fs.onNext(3); 133 | fs.onComplete(); 134 | 135 | tc.assertResult(1, 2, 3); 136 | 137 | Assert.assertNull(state[1]); 138 | } 139 | 140 | @Test 141 | public void flowToReactiveStreamsSubscriber() { 142 | TestEitherConsumer tc = new TestEitherConsumer(); 143 | 144 | org.reactivestreams.Subscriber fs = FlowAdapters.toSubscriber(tc); 145 | 146 | final Object[] state = { null, null }; 147 | 148 | fs.onSubscribe(new org.reactivestreams.Subscription() { 149 | @Override 150 | public void request(long n) { 151 | state[0] = n; 152 | } 153 | 154 | @Override 155 | public void cancel() { 156 | state[1] = true; 157 | } 158 | }); 159 | 160 | Assert.assertEquals(state[0], Long.MAX_VALUE); 161 | 162 | fs.onNext(1); 163 | fs.onNext(2); 164 | fs.onNext(3); 165 | fs.onComplete(); 166 | 167 | tc.assertResult(1, 2, 3); 168 | 169 | Assert.assertNull(state[1]); 170 | } 171 | 172 | @Test 173 | public void stableConversionForSubscriber() { 174 | Subscriber rsSub = new Subscriber() { 175 | @Override public void onSubscribe(Subscription s) {}; 176 | @Override public void onNext(Integer i) {}; 177 | @Override public void onError(Throwable t) {}; 178 | @Override public void onComplete() {}; 179 | }; 180 | 181 | Flow.Subscriber fSub = new Flow.Subscriber() { 182 | @Override public void onSubscribe(Flow.Subscription s) {}; 183 | @Override public void onNext(Integer i) {}; 184 | @Override public void onError(Throwable t) {}; 185 | @Override public void onComplete() {}; 186 | }; 187 | 188 | Assert.assertSame(FlowAdapters.toSubscriber(FlowAdapters.toFlowSubscriber(rsSub)), rsSub); 189 | Assert.assertSame(FlowAdapters.toFlowSubscriber(FlowAdapters.toSubscriber(fSub)), fSub); 190 | } 191 | 192 | @Test 193 | public void stableConversionForProcessor() { 194 | Processor rsPro = new Processor() { 195 | @Override public void onSubscribe(Subscription s) {}; 196 | @Override public void onNext(Integer i) {}; 197 | @Override public void onError(Throwable t) {}; 198 | @Override public void onComplete() {}; 199 | @Override public void subscribe(Subscriber s) {}; 200 | }; 201 | 202 | Flow.Processor fPro = new Flow.Processor() { 203 | @Override public void onSubscribe(Flow.Subscription s) {}; 204 | @Override public void onNext(Integer i) {}; 205 | @Override public void onError(Throwable t) {}; 206 | @Override public void onComplete() {}; 207 | @Override public void subscribe(Flow.Subscriber s) {}; 208 | }; 209 | 210 | Assert.assertSame(FlowAdapters.toProcessor(FlowAdapters.toFlowProcessor(rsPro)), rsPro); 211 | Assert.assertSame(FlowAdapters.toFlowProcessor(FlowAdapters.toProcessor(fPro)), fPro); 212 | } 213 | 214 | @Test 215 | public void stableConversionForPublisher() { 216 | Publisher rsPub = new Publisher() { 217 | @Override public void subscribe(Subscriber s) {}; 218 | }; 219 | 220 | Flow.Publisher fPub = new Flow.Publisher() { 221 | @Override public void subscribe(Flow.Subscriber s) {}; 222 | }; 223 | 224 | Assert.assertSame(FlowAdapters.toPublisher(FlowAdapters.toFlowPublisher(rsPub)), rsPub); 225 | Assert.assertSame(FlowAdapters.toFlowPublisher(FlowAdapters.toPublisher(fPub)), fPub); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/MulticastPublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | import java.lang.invoke.MethodHandles; 8 | import java.lang.invoke.VarHandle; 9 | import java.util.Objects; 10 | import java.util.concurrent.Executor; 11 | import java.util.concurrent.Flow; 12 | import java.util.concurrent.Flow.*; 13 | import java.util.concurrent.ForkJoinPool; 14 | import java.util.concurrent.atomic.*; 15 | 16 | final class MulticastPublisher implements Publisher, AutoCloseable { 17 | 18 | final Executor executor; 19 | final int bufferSize; 20 | 21 | final AtomicBoolean done = new AtomicBoolean(); 22 | Throwable error; 23 | 24 | static final InnerSubscription[] EMPTY = new InnerSubscription[0]; 25 | static final InnerSubscription[] TERMINATED = new InnerSubscription[0]; 26 | 27 | 28 | final AtomicReference[]> subscribers = new AtomicReference[]>(); 29 | 30 | public MulticastPublisher() { 31 | this(ForkJoinPool.commonPool(), Flow.defaultBufferSize()); 32 | } 33 | 34 | @SuppressWarnings("unchecked") 35 | public MulticastPublisher(Executor executor, int bufferSize) { 36 | if ((bufferSize & (bufferSize - 1)) != 0) { 37 | throw new IllegalArgumentException("Please provide a power-of-two buffer size"); 38 | } 39 | this.executor = executor; 40 | this.bufferSize = bufferSize; 41 | subscribers.setRelease(EMPTY); 42 | } 43 | 44 | public boolean offer(T item) { 45 | Objects.requireNonNull(item, "item is null"); 46 | 47 | InnerSubscription[] a = subscribers.get(); 48 | synchronized (this) { 49 | for (InnerSubscription inner : a) { 50 | if (inner.isFull()) { 51 | return false; 52 | } 53 | } 54 | for (InnerSubscription inner : a) { 55 | inner.offer(item); 56 | inner.drain(executor); 57 | } 58 | } 59 | 60 | return true; 61 | } 62 | 63 | @SuppressWarnings("unchecked") 64 | public void complete() { 65 | if (done.compareAndSet(false, true)) { 66 | for (InnerSubscription inner : subscribers.getAndSet(TERMINATED)) { 67 | inner.done = true; 68 | inner.drain(executor); 69 | } 70 | } 71 | } 72 | 73 | @SuppressWarnings("unchecked") 74 | public void completeExceptionally(Throwable error) { 75 | if (done.compareAndSet(false, true)) { 76 | this.error = error; 77 | for (InnerSubscription inner : subscribers.getAndSet(TERMINATED)) { 78 | inner.error = error; 79 | inner.done = true; 80 | inner.drain(executor); 81 | } 82 | } 83 | } 84 | 85 | @Override 86 | public void close() { 87 | complete(); 88 | } 89 | 90 | @Override 91 | public void subscribe(Subscriber subscriber) { 92 | Objects.requireNonNull(subscriber, "subscriber is null"); 93 | InnerSubscription inner = new InnerSubscription(subscriber, bufferSize, this); 94 | if (!add(inner)) { 95 | Throwable ex = error; 96 | if (ex != null) { 97 | inner.error = ex; 98 | } 99 | inner.done = true; 100 | } 101 | inner.drain(executor); 102 | } 103 | 104 | public boolean hasSubscribers() { 105 | return subscribers.get().length != 0; 106 | } 107 | 108 | boolean add(InnerSubscription inner) { 109 | 110 | for (;;) { 111 | InnerSubscription[] a = subscribers.get(); 112 | if (a == TERMINATED) { 113 | return false; 114 | } 115 | 116 | int n = a.length; 117 | @SuppressWarnings("unchecked") 118 | InnerSubscription[] b = new InnerSubscription[n + 1]; 119 | System.arraycopy(a, 0, b, 0, n); 120 | b[n] = inner; 121 | if (subscribers.compareAndSet(a, b)) { 122 | return true; 123 | } 124 | } 125 | } 126 | 127 | @SuppressWarnings("unchecked") 128 | void remove(InnerSubscription inner) { 129 | for (;;) { 130 | InnerSubscription[] a = subscribers.get(); 131 | int n = a.length; 132 | if (n == 0) { 133 | break; 134 | } 135 | 136 | int j = -1; 137 | for (int i = 0; i < n; i++) { 138 | if (a[i] == inner) { 139 | j = i; 140 | break; 141 | } 142 | } 143 | if (j < 0) { 144 | break; 145 | } 146 | InnerSubscription[] b; 147 | if (n == 1) { 148 | b = EMPTY; 149 | } else { 150 | b = new InnerSubscription[n - 1]; 151 | System.arraycopy(a, 0, b, 0, j); 152 | System.arraycopy(a, j + 1, b, j, n - j - 1); 153 | } 154 | if (subscribers.compareAndSet(a, b)) { 155 | break; 156 | } 157 | } 158 | } 159 | 160 | static final class InnerSubscription implements Subscription, Runnable { 161 | 162 | final Subscriber actual; 163 | final MulticastPublisher parent; 164 | final AtomicReferenceArray queue; 165 | final int mask; 166 | 167 | volatile boolean badRequest; 168 | final AtomicBoolean cancelled = new AtomicBoolean(); 169 | 170 | volatile boolean done; 171 | Throwable error; 172 | 173 | boolean subscribed; 174 | long emitted; 175 | 176 | final AtomicLong requested = new AtomicLong(); 177 | 178 | final AtomicInteger wip = new AtomicInteger(); 179 | 180 | final AtomicLong producerIndex = new AtomicLong(); 181 | 182 | final AtomicLong consumerIndex = new AtomicLong(); 183 | 184 | InnerSubscription(Subscriber actual, int bufferSize, MulticastPublisher parent) { 185 | this.actual = actual; 186 | this.queue = new AtomicReferenceArray(bufferSize); 187 | this.parent = parent; 188 | this.mask = bufferSize - 1; 189 | } 190 | 191 | void offer(T item) { 192 | AtomicReferenceArray q = queue; 193 | int m = mask; 194 | long pi = producerIndex.get(); 195 | int offset = (int)(pi) & m; 196 | 197 | q.setRelease(offset, item); 198 | producerIndex.setRelease(pi + 1); 199 | } 200 | 201 | T poll() { 202 | AtomicReferenceArray q = queue; 203 | int m = mask; 204 | long ci = consumerIndex.get(); 205 | 206 | int offset = (int)(ci) & m; 207 | T o = q.getAcquire(offset); 208 | if (o != null) { 209 | q.setRelease(offset, null); 210 | consumerIndex.setRelease(ci + 1); 211 | } 212 | return o; 213 | } 214 | 215 | boolean isFull() { 216 | return 1 + mask + consumerIndex.get() == producerIndex.get(); 217 | } 218 | 219 | void drain(Executor executor) { 220 | if (wip.getAndAdd(1) == 0) { 221 | executor.execute(this); 222 | } 223 | } 224 | 225 | @Override 226 | public void request(long n) { 227 | if (n <= 0L) { 228 | badRequest = true; 229 | done = true; 230 | } else { 231 | for (;;) { 232 | long r = requested.get(); 233 | long u = r + n; 234 | if (u < 0) { 235 | u = Long.MAX_VALUE; 236 | } 237 | if (requested.compareAndSet(r, u)) { 238 | break; 239 | } 240 | } 241 | } 242 | drain(parent.executor); 243 | } 244 | 245 | @Override 246 | public void cancel() { 247 | if (cancelled.compareAndSet(false, true)) { 248 | parent.remove(this); 249 | } 250 | } 251 | 252 | void clear() { 253 | error = null; 254 | while (poll() != null) ; 255 | } 256 | 257 | @Override 258 | public void run() { 259 | int missed = 1; 260 | Subscriber a = actual; 261 | 262 | outer: 263 | for (;;) { 264 | 265 | if (subscribed) { 266 | if (cancelled.get()) { 267 | clear(); 268 | } else { 269 | long r = requested.get(); 270 | long e = emitted; 271 | 272 | while (e != r) { 273 | if (cancelled.get()) { 274 | continue outer; 275 | } 276 | 277 | boolean d = done; 278 | 279 | if (d) { 280 | Throwable ex = error; 281 | if (ex != null) { 282 | cancelled.setRelease(true); 283 | a.onError(ex); 284 | continue outer; 285 | } 286 | if (badRequest) { 287 | cancelled.setRelease(true); 288 | parent.remove(this); 289 | a.onError(new IllegalArgumentException("§3.9 violated: request was not positive")); 290 | continue outer; 291 | } 292 | } 293 | 294 | T v = poll(); 295 | boolean empty = v == null; 296 | 297 | if (d && empty) { 298 | cancelled.setRelease(true); 299 | a.onComplete(); 300 | break; 301 | } 302 | 303 | if (empty) { 304 | break; 305 | } 306 | 307 | a.onNext(v); 308 | 309 | e++; 310 | } 311 | 312 | if (e == r) { 313 | if (cancelled.get()) { 314 | continue outer; 315 | } 316 | if (done) { 317 | Throwable ex = error; 318 | if (ex != null) { 319 | cancelled.setRelease(true); 320 | a.onError(ex); 321 | } else 322 | if (badRequest) { 323 | cancelled.setRelease(true); 324 | a.onError(new IllegalArgumentException("§3.9 violated: request was not positive")); 325 | } else 326 | if (producerIndex == consumerIndex) { 327 | cancelled.setRelease(true); 328 | a.onComplete(); 329 | } 330 | } 331 | } 332 | 333 | emitted = e; 334 | } 335 | } else { 336 | subscribed = true; 337 | a.onSubscribe(this); 338 | } 339 | 340 | int w = wip.get(); 341 | if (missed == w) { 342 | w = wip.getAndAdd(-missed); 343 | if (missed == w) { 344 | break; 345 | } 346 | } 347 | missed = w; 348 | } 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/SubmissionPublisherTckTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | import org.reactivestreams.tck.PublisherVerification; 8 | import org.reactivestreams.tck.TestEnvironment; 9 | import org.testng.annotations.Test; 10 | 11 | import java.io.IOException; 12 | import java.util.concurrent.SubmissionPublisher; 13 | 14 | @Test 15 | public class SubmissionPublisherTckTest extends PublisherVerification { 16 | 17 | public SubmissionPublisherTckTest() { 18 | super(new TestEnvironment(300)); 19 | } 20 | 21 | @Override 22 | public Publisher createPublisher(final long elements) { 23 | final SubmissionPublisher sp = new SubmissionPublisher(); 24 | new Thread(new Runnable() { 25 | @Override 26 | public void run() { 27 | while (!sp.hasSubscribers()) { 28 | Thread.yield(); 29 | } 30 | for (int i = 0; i < elements; i++) { 31 | sp.submit(i); 32 | } 33 | sp.close(); 34 | } 35 | }).start(); 36 | return FlowAdapters.toPublisher(sp); 37 | } 38 | 39 | @Override 40 | public Publisher createFailedPublisher() { 41 | final SubmissionPublisher sp = new SubmissionPublisher(); 42 | sp.closeExceptionally(new IOException()); 43 | return FlowAdapters.toPublisher(sp); 44 | } 45 | 46 | @Override 47 | public long maxElementsFromPublisher() { 48 | return 100; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/TestEitherConsumer.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams; 6 | 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.concurrent.CountDownLatch; 11 | import java.util.concurrent.Flow; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * Class that provides basic state assertions on received elements and 16 | * terminal signals from either a Reactive Streams Publisher or a 17 | * Flow Publisher. 18 | *

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

23 | * @param the element type 24 | */ 25 | class TestEitherConsumer implements Flow.Subscriber, Subscriber { 26 | 27 | protected final List values; 28 | 29 | protected final List errors; 30 | 31 | protected int completions; 32 | 33 | protected Flow.Subscription subscription; 34 | 35 | protected Subscription subscriptionRs; 36 | 37 | protected final CountDownLatch done; 38 | 39 | final long initialRequest; 40 | 41 | public TestEitherConsumer() { 42 | this(Long.MAX_VALUE); 43 | } 44 | 45 | public TestEitherConsumer(long initialRequest) { 46 | this.values = new ArrayList(); 47 | this.errors = new ArrayList(); 48 | this.done = new CountDownLatch(1); 49 | this.initialRequest = initialRequest; 50 | } 51 | 52 | @Override 53 | public final void onSubscribe(Flow.Subscription s) { 54 | this.subscription = s; 55 | s.request(initialRequest); 56 | } 57 | 58 | @Override 59 | public void onSubscribe(Subscription s) { 60 | this.subscriptionRs = s; 61 | s.request(initialRequest); 62 | } 63 | 64 | @Override 65 | public void onNext(T item) { 66 | values.add(item); 67 | if (subscription == null && subscriptionRs == null) { 68 | errors.add(new IllegalStateException("onSubscribe not called")); 69 | } 70 | } 71 | 72 | @Override 73 | public void onError(Throwable throwable) { 74 | errors.add(throwable); 75 | if (subscription == null && subscriptionRs == null) { 76 | errors.add(new IllegalStateException("onSubscribe not called")); 77 | } 78 | done.countDown(); 79 | } 80 | 81 | @Override 82 | public void onComplete() { 83 | completions++; 84 | if (subscription == null && subscriptionRs == null) { 85 | errors.add(new IllegalStateException("onSubscribe not called")); 86 | } 87 | done.countDown(); 88 | } 89 | 90 | public final void cancel() { 91 | // FIXME implement deferred cancellation 92 | } 93 | 94 | public final List values() { 95 | return values; 96 | } 97 | 98 | public final List errors() { 99 | return errors; 100 | } 101 | 102 | public final int completions() { 103 | return completions; 104 | } 105 | 106 | public final boolean await(long timeout, TimeUnit unit) throws InterruptedException { 107 | return done.await(timeout, unit); 108 | } 109 | 110 | public final TestEitherConsumer assertResult(T... items) { 111 | if (!values.equals(Arrays.asList(items))) { 112 | throw new AssertionError("Expected: " + Arrays.toString(items) + ", Actual: " + values + ", Completions: " + completions); 113 | } 114 | if (completions != 1) { 115 | throw new AssertionError("Not completed: " + completions); 116 | } 117 | return this; 118 | } 119 | 120 | 121 | public final TestEitherConsumer assertFailure(Class errorClass, T... items) { 122 | if (!values.equals(Arrays.asList(items))) { 123 | throw new AssertionError("Expected: " + Arrays.toString(items) + ", Actual: " + values + ", Completions: " + completions); 124 | } 125 | if (completions != 0) { 126 | throw new AssertionError("Completed: " + completions); 127 | } 128 | if (errors.isEmpty()) { 129 | throw new AssertionError("No errors"); 130 | } 131 | if (!errorClass.isInstance(errors.get(0))) { 132 | AssertionError ae = new AssertionError("Wrong throwable"); 133 | ae.initCause(errors.get(0)); 134 | throw ae; 135 | } 136 | return this; 137 | } 138 | 139 | public final TestEitherConsumer awaitDone(long timeout, TimeUnit unit) { 140 | try { 141 | if (!done.await(timeout, unit)) { 142 | subscription.cancel(); 143 | throw new RuntimeException("Timed out. Values: " + values.size() 144 | + ", Errors: " + errors.size() + ", Completions: " + completions); 145 | } 146 | } catch (InterruptedException ex) { 147 | throw new RuntimeException("Interrupted"); 148 | } 149 | return this; 150 | } 151 | 152 | public final TestEitherConsumer assertRange(int start, int count) { 153 | if (values.size() != count) { 154 | throw new AssertionError("Expected: " + count + ", Actual: " + values.size()); 155 | } 156 | for (int i = 0; i < count; i++) { 157 | if ((Integer)values.get(i) != start + i) { 158 | throw new AssertionError("Index: " + i + ", Expected: " 159 | + (i + start) + ", Actual: " +values.get(i)); 160 | } 161 | } 162 | if (completions != 1) { 163 | throw new AssertionError("Not completed: " + completions); 164 | } 165 | return this; 166 | } 167 | } -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/EmptyLazyFlowPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.FlowAdapters; 8 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 9 | import java.util.concurrent.Flow.Publisher; 10 | 11 | import org.reactivestreams.tck.TestEnvironment; 12 | import org.testng.annotations.AfterClass; 13 | import org.testng.annotations.BeforeClass; 14 | import org.testng.annotations.Test; 15 | 16 | import java.util.Collections; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | 20 | @Test 21 | public class EmptyLazyFlowPublisherTest extends FlowPublisherVerification { 22 | 23 | private ExecutorService ex; 24 | 25 | public EmptyLazyFlowPublisherTest() { 26 | super(new TestEnvironment()); 27 | } 28 | 29 | @BeforeClass 30 | void before() { ex = Executors.newFixedThreadPool(4); } 31 | 32 | @AfterClass 33 | void after() { if (ex != null) ex.shutdown(); } 34 | 35 | @Override 36 | public Publisher createFlowPublisher(long elements) { 37 | return FlowAdapters.toFlowPublisher( 38 | new AsyncIterablePublisher(Collections.emptyList(), ex) 39 | ); 40 | } 41 | 42 | @Override 43 | public Publisher createFailedFlowPublisher() { 44 | return null; 45 | } 46 | 47 | @Override 48 | public long maxElementsFromPublisher() { 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/LockstepFlowProcessorTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.*; 8 | import org.reactivestreams.tck.*; 9 | import org.testng.annotations.Test; 10 | 11 | import java.util.concurrent.*; 12 | import java.util.concurrent.atomic.*; 13 | 14 | @Test 15 | public class LockstepFlowProcessorTest extends IdentityFlowProcessorVerification { 16 | 17 | public LockstepFlowProcessorTest() { 18 | super(new TestEnvironment()); 19 | } 20 | @Override 21 | public Flow.Processor createIdentityFlowProcessor(int bufferSize) { 22 | return new LockstepProcessor(); 23 | } 24 | 25 | @Override 26 | public Flow.Publisher createFailedFlowPublisher() { 27 | LockstepProcessor proc = new LockstepProcessor(); 28 | proc.onError(new Exception()); 29 | return proc; 30 | } 31 | 32 | @Override 33 | public ExecutorService publisherExecutorService() { 34 | return Executors.newCachedThreadPool(); 35 | } 36 | 37 | @Override 38 | public Integer createElement(int element) { 39 | return element; 40 | } 41 | 42 | @Override 43 | public long maxSupportedSubscribers() { 44 | return 2; 45 | } 46 | 47 | @Override 48 | public boolean doesCoordinatedEmission() { 49 | return true; 50 | } 51 | 52 | static final class LockstepProcessor implements Flow.Processor { 53 | 54 | final AtomicReference[]> subscribers = 55 | new AtomicReference[]>(EMPTY); 56 | 57 | static final LockstepSubscription[] EMPTY = new LockstepSubscription[0]; 58 | static final LockstepSubscription[] TERMINATED = new LockstepSubscription[0]; 59 | 60 | volatile boolean done; 61 | Throwable error; 62 | 63 | final AtomicReference upstream = 64 | new AtomicReference(); 65 | 66 | final AtomicReferenceArray queue = 67 | new AtomicReferenceArray(BUFFER_MASK + 1); 68 | 69 | final AtomicLong producerIndex = new AtomicLong(); 70 | 71 | final AtomicLong consumerIndex = new AtomicLong(); 72 | 73 | final AtomicInteger wip = new AtomicInteger(); 74 | 75 | static final int BUFFER_MASK = 127; 76 | 77 | int consumed; 78 | 79 | @Override 80 | public void subscribe(Flow.Subscriber s) { 81 | LockstepSubscription subscription = new LockstepSubscription(s, this); 82 | s.onSubscribe(subscription); 83 | if (add(subscription)) { 84 | if (subscription.isCancelled()) { 85 | remove(subscription); 86 | } else { 87 | drain(); 88 | } 89 | } else { 90 | Throwable ex = error; 91 | if (ex != null) { 92 | s.onError(ex); 93 | } else { 94 | s.onComplete(); 95 | } 96 | } 97 | } 98 | 99 | boolean add(LockstepSubscription sub) { 100 | for (;;) { 101 | LockstepSubscription[] a = subscribers.get(); 102 | if (a == TERMINATED) { 103 | return false; 104 | } 105 | int n = a.length; 106 | LockstepSubscription[] b = new LockstepSubscription[n + 1]; 107 | System.arraycopy(a, 0, b, 0, n); 108 | b[n] = sub; 109 | if (subscribers.compareAndSet(a, b)) { 110 | return true; 111 | } 112 | } 113 | } 114 | 115 | void remove(LockstepSubscription sub) { 116 | for (;;) { 117 | LockstepSubscription[] a = subscribers.get(); 118 | int n = a.length; 119 | 120 | if (n == 0) { 121 | break; 122 | } 123 | 124 | int j = -1; 125 | for (int i = 0; i < n; i++) { 126 | if (a[i] == sub) { 127 | j = i; 128 | break; 129 | } 130 | } 131 | 132 | if (j < 0) { 133 | break; 134 | } 135 | LockstepSubscription[] b; 136 | if (n == 1) { 137 | b = TERMINATED; 138 | } else { 139 | b = new LockstepSubscription[n - 1]; 140 | System.arraycopy(a, 0, b, 0, j); 141 | System.arraycopy(a, j + 1, b, j, n - j - 1); 142 | } 143 | if (subscribers.compareAndSet(a, b)) { 144 | if (b == TERMINATED) { 145 | Flow.Subscription s = upstream.getAndSet(CancelledSubscription.INSTANCE); 146 | if (s != null) { 147 | s.cancel(); 148 | } 149 | } 150 | break; 151 | } 152 | } 153 | } 154 | 155 | @Override 156 | public void onSubscribe(Flow.Subscription s) { 157 | if (upstream.compareAndSet(null, s)) { 158 | s.request(BUFFER_MASK + 1); 159 | } else { 160 | s.cancel(); 161 | } 162 | } 163 | 164 | @Override 165 | public void onNext(T t) { 166 | if (t == null) { 167 | throw new NullPointerException("t == null"); 168 | } 169 | long pi = producerIndex.get(); 170 | queue.lazySet((int)pi & BUFFER_MASK, t); 171 | producerIndex.lazySet(pi + 1); 172 | drain(); 173 | } 174 | 175 | @Override 176 | public void onError(Throwable t) { 177 | if (t == null) { 178 | throw new NullPointerException("t == null"); 179 | } 180 | error = t; 181 | done = true; 182 | drain(); 183 | } 184 | 185 | @Override 186 | public void onComplete() { 187 | done = true; 188 | drain(); 189 | } 190 | 191 | void drain() { 192 | if (wip.getAndIncrement() != 0) { 193 | return; 194 | } 195 | 196 | int limit = (BUFFER_MASK + 1) - ((BUFFER_MASK + 1) >> 2); 197 | int missed = 1; 198 | for (;;) { 199 | 200 | for (;;) { 201 | LockstepSubscription[] subscribers = this.subscribers.get(); 202 | int n = subscribers.length; 203 | 204 | long ci = consumerIndex.get(); 205 | 206 | boolean d = done; 207 | boolean empty = producerIndex.get() == ci; 208 | 209 | if (d) { 210 | Throwable ex = error; 211 | if (ex != null) { 212 | for (LockstepSubscription sub : this.subscribers.getAndSet(TERMINATED)) { 213 | sub.subscriber.onError(ex); 214 | } 215 | break; 216 | } else if (empty) { 217 | for (LockstepSubscription sub : this.subscribers.getAndSet(TERMINATED)) { 218 | sub.subscriber.onComplete(); 219 | } 220 | break; 221 | } 222 | } 223 | 224 | if (n != 0 && !empty) { 225 | long ready = Long.MAX_VALUE; 226 | int c = 0; 227 | for (LockstepSubscription sub : subscribers) { 228 | long req = sub.get(); 229 | if (req != Long.MIN_VALUE) { 230 | ready = Math.min(ready, req - sub.emitted); 231 | c++; 232 | } 233 | } 234 | 235 | if (ready != 0 && c != 0) { 236 | int offset = (int) ci & BUFFER_MASK; 237 | T value = queue.get(offset); 238 | queue.lazySet(offset, null); 239 | consumerIndex.lazySet(ci + 1); 240 | 241 | for (LockstepSubscription sub : subscribers) { 242 | sub.subscriber.onNext(value); 243 | sub.emitted++; 244 | } 245 | 246 | if (++consumed == limit) { 247 | consumed = 0; 248 | upstream.get().request(limit); 249 | } 250 | } else { 251 | break; 252 | } 253 | } else { 254 | break; 255 | } 256 | } 257 | 258 | missed = wip.addAndGet(-missed); 259 | if (missed == 0) { 260 | break; 261 | } 262 | } 263 | } 264 | 265 | static final class LockstepSubscription extends AtomicLong 266 | implements Flow.Subscription { 267 | 268 | final Flow.Subscriber subscriber; 269 | 270 | final LockstepProcessor parent; 271 | 272 | long emitted; 273 | 274 | LockstepSubscription(Flow.Subscriber subscriber, LockstepProcessor parent) { 275 | this.subscriber = subscriber; 276 | this.parent = parent; 277 | } 278 | 279 | @Override 280 | public void request(long n) { 281 | if (n <= 0L) { 282 | cancel(); 283 | subscriber.onError(new IllegalArgumentException("§3.9 violated: positive request amount required")); 284 | return; 285 | } 286 | for (;;) { 287 | long current = get(); 288 | if (current == Long.MIN_VALUE || current == Long.MAX_VALUE) { 289 | break; 290 | } 291 | 292 | long updated = current + n; 293 | if (updated < 0L) { 294 | updated = Long.MAX_VALUE; 295 | } 296 | if (compareAndSet(current, updated)) { 297 | parent.drain(); 298 | break; 299 | } 300 | } 301 | } 302 | 303 | @Override 304 | public void cancel() { 305 | if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { 306 | parent.remove(this); 307 | parent.drain(); 308 | } 309 | } 310 | 311 | boolean isCancelled() { 312 | return get() == Long.MIN_VALUE; 313 | } 314 | } 315 | } 316 | 317 | enum CancelledSubscription implements Flow.Subscription { 318 | 319 | INSTANCE; 320 | 321 | @Override 322 | public void request(long n) { 323 | // Subscription already cancelled 324 | } 325 | 326 | @Override 327 | public void cancel() { 328 | // Subscription already cancelled 329 | } 330 | } 331 | } 332 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/RangeFlowPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.Flow; 10 | import java.util.concurrent.atomic.*; 11 | 12 | import org.reactivestreams.tck.TestEnvironment; 13 | import org.testng.annotations.*; 14 | 15 | @Test 16 | public class RangeFlowPublisherTest extends FlowPublisherVerification { 17 | 18 | static final Map stacks = new ConcurrentHashMap(); 19 | 20 | static final Map states = new ConcurrentHashMap(); 21 | 22 | static final AtomicInteger id = new AtomicInteger(); 23 | 24 | @AfterClass 25 | public static void afterClass() { 26 | boolean fail = false; 27 | StringBuilder b = new StringBuilder(); 28 | for (Map.Entry t : states.entrySet()) { 29 | if (!t.getValue()) { 30 | b.append("\r\n-------------------------------"); 31 | for (Object o : stacks.get(t.getKey())) { 32 | b.append("\r\nat ").append(o); 33 | } 34 | fail = true; 35 | } 36 | } 37 | if (fail) { 38 | throw new AssertionError("Cancellations were missing:" + b); 39 | } 40 | } 41 | 42 | public RangeFlowPublisherTest() { 43 | super(new TestEnvironment()); 44 | } 45 | 46 | @Override 47 | public Flow.Publisher createFlowPublisher(long elements) { 48 | return new RangeFlowPublisher(1, elements); 49 | } 50 | 51 | @Override 52 | public Flow.Publisher createFailedFlowPublisher() { 53 | return null; 54 | } 55 | 56 | static final class RangeFlowPublisher 57 | implements Flow.Publisher { 58 | 59 | final StackTraceElement[] stacktrace; 60 | 61 | final long start; 62 | 63 | final long count; 64 | 65 | RangeFlowPublisher(long start, long count) { 66 | this.stacktrace = Thread.currentThread().getStackTrace(); 67 | this.start = start; 68 | this.count = count; 69 | } 70 | 71 | @Override 72 | public void subscribe(Flow.Subscriber s) { 73 | if (s == null) { 74 | throw new NullPointerException(); 75 | } 76 | 77 | int ids = id.incrementAndGet(); 78 | 79 | RangeFlowSubscription parent = new RangeFlowSubscription(s, ids, start, start + count); 80 | stacks.put(ids, stacktrace); 81 | states.put(ids, false); 82 | s.onSubscribe(parent); 83 | } 84 | 85 | static final class RangeFlowSubscription extends AtomicLong implements Flow.Subscription { 86 | 87 | private static final long serialVersionUID = 9066221863682220604L; 88 | 89 | final Flow.Subscriber actual; 90 | 91 | final int ids; 92 | 93 | final long end; 94 | 95 | long index; 96 | 97 | volatile boolean cancelled; 98 | 99 | RangeFlowSubscription(Flow.Subscriber actual, int ids, long start, long end) { 100 | this.actual = actual; 101 | this.ids = ids; 102 | this.index = start; 103 | this.end = end; 104 | } 105 | 106 | @Override 107 | public void request(long n) { 108 | if (!cancelled) { 109 | if (n <= 0L) { 110 | cancelled = true; 111 | states.put(ids, true); 112 | actual.onError(new IllegalArgumentException("§3.9 violated")); 113 | return; 114 | } 115 | 116 | for (;;) { 117 | long r = get(); 118 | long u = r + n; 119 | if (u < 0L) { 120 | u = Long.MAX_VALUE; 121 | } 122 | if (compareAndSet(r, u)) { 123 | if (r == 0) { 124 | break; 125 | } 126 | return; 127 | } 128 | } 129 | 130 | long idx = index; 131 | long f = end; 132 | 133 | for (;;) { 134 | long e = 0; 135 | while (e != n && idx != f) { 136 | if (cancelled) { 137 | return; 138 | } 139 | 140 | actual.onNext((int)idx); 141 | 142 | idx++; 143 | e++; 144 | } 145 | 146 | if (idx == f) { 147 | if (!cancelled) { 148 | states.put(ids, true); 149 | actual.onComplete(); 150 | } 151 | return; 152 | } 153 | 154 | index = idx; 155 | n = addAndGet(-n); 156 | if (n == 0) { 157 | break; 158 | } 159 | } 160 | } 161 | } 162 | 163 | @Override 164 | public void cancel() { 165 | cancelled = true; 166 | states.put(ids, true); 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/SingleElementFlowPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import java.util.concurrent.Flow; 8 | import java.util.concurrent.Flow.Publisher; 9 | 10 | import org.reactivestreams.FlowAdapters; 11 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 12 | import org.reactivestreams.tck.TestEnvironment; 13 | import org.testng.annotations.AfterClass; 14 | import org.testng.annotations.BeforeClass; 15 | import org.testng.annotations.Test; 16 | 17 | import java.util.Collections; 18 | import java.util.concurrent.ExecutorService; 19 | import java.util.concurrent.Executors; 20 | 21 | @Test 22 | public class SingleElementFlowPublisherTest extends FlowPublisherVerification { 23 | 24 | private ExecutorService ex; 25 | 26 | public SingleElementFlowPublisherTest() { 27 | super(new TestEnvironment()); 28 | } 29 | 30 | @BeforeClass 31 | void before() { ex = Executors.newFixedThreadPool(4); } 32 | 33 | @AfterClass 34 | void after() { if (ex != null) ex.shutdown(); } 35 | 36 | @Override 37 | public Flow.Publisher createFlowPublisher(long elements) { 38 | return FlowAdapters.toFlowPublisher(new AsyncIterablePublisher(Collections.singleton(1), ex)); 39 | } 40 | 41 | @Override 42 | public Publisher createFailedFlowPublisher() { 43 | return null; 44 | } 45 | 46 | @Override 47 | public long maxElementsFromPublisher() { 48 | return 1; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/SubmissionPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.tck.TestEnvironment; 8 | import org.testng.annotations.Test; 9 | 10 | import java.util.concurrent.*; 11 | 12 | @Test 13 | public class SubmissionPublisherTest extends FlowPublisherVerification { 14 | 15 | public SubmissionPublisherTest() { 16 | super(new TestEnvironment()); 17 | } 18 | 19 | @Override 20 | public Flow.Publisher createFlowPublisher(final long elements) { 21 | final SubmissionPublisher sp = new SubmissionPublisher(); 22 | 23 | Thread t = new Thread(new Runnable() { 24 | @Override 25 | public void run() { 26 | while (sp.getNumberOfSubscribers() == 0) { 27 | try { 28 | Thread.sleep(1); 29 | } catch (InterruptedException ex) { 30 | return; 31 | } 32 | } 33 | 34 | for (int i = 0; i < elements; i++) { 35 | sp.submit(i); 36 | } 37 | 38 | sp.close(); 39 | } 40 | }); 41 | t.setDaemon(true); 42 | t.start(); 43 | 44 | return sp; 45 | } 46 | 47 | @Override 48 | public Flow.Publisher createFailedFlowPublisher() { 49 | SubmissionPublisher sp = new SubmissionPublisher(); 50 | sp.closeExceptionally(new Exception()); 51 | return sp; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.tck.TestEnvironment; 8 | import org.reactivestreams.tck.flow.FlowSubscriberBlackboxVerification; 9 | import org.reactivestreams.tck.flow.support.SyncTriggeredDemandFlowSubscriber; 10 | import org.testng.annotations.AfterClass; 11 | import org.testng.annotations.BeforeClass; 12 | import org.testng.annotations.Test; 13 | 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.Flow; 17 | 18 | @Test // Must be here for TestNG to find and run this, do not remove 19 | public class SyncTriggeredDemandSubscriberTest extends FlowSubscriberBlackboxVerification { 20 | 21 | private ExecutorService e; 22 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 23 | @AfterClass void after() { if (e != null) e.shutdown(); } 24 | 25 | public SyncTriggeredDemandSubscriberTest() { 26 | super(new TestEnvironment()); 27 | } 28 | 29 | @Test(enabled = false) 30 | @Override 31 | public void triggerFlowRequest(Flow.Subscriber subscriber) { 32 | ((SyncTriggeredDemandFlowSubscriber) subscriber).triggerDemand(1); 33 | } 34 | 35 | @Override public Flow.Subscriber createFlowSubscriber() { 36 | return new SyncTriggeredDemandFlowSubscriber() { 37 | private long acc; 38 | @Override protected long foreach(final Integer element) { 39 | acc += element; 40 | return 1; 41 | } 42 | 43 | @Override public void onComplete() { 44 | } 45 | }; 46 | } 47 | 48 | @Override public Integer createElement(int element) { 49 | return element; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/SyncTriggeredDemandSubscriberWhiteboxTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | import org.reactivestreams.tck.TestEnvironment; 10 | import org.reactivestreams.tck.flow.support.SyncTriggeredDemandFlowSubscriber; 11 | import org.testng.annotations.AfterClass; 12 | import org.testng.annotations.BeforeClass; 13 | import org.testng.annotations.Test; 14 | 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | import java.util.concurrent.Flow; 18 | 19 | @Test // Must be here for TestNG to find and run this, do not remove 20 | public class SyncTriggeredDemandSubscriberWhiteboxTest extends FlowSubscriberWhiteboxVerification { 21 | 22 | private ExecutorService e; 23 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 24 | @AfterClass void after() { if (e != null) e.shutdown(); } 25 | 26 | public SyncTriggeredDemandSubscriberWhiteboxTest() { 27 | super(new TestEnvironment()); 28 | } 29 | 30 | @Override 31 | public Flow.Subscriber createFlowSubscriber(final WhiteboxSubscriberProbe probe) { 32 | return new SyncTriggeredDemandFlowSubscriber() { 33 | @Override 34 | public void onSubscribe(final Flow.Subscription s) { 35 | super.onSubscribe(s); 36 | 37 | probe.registerOnSubscribe(new SubscriberPuppet() { 38 | @Override 39 | public void triggerRequest(long elements) { 40 | s.request(elements); 41 | } 42 | 43 | @Override 44 | public void signalCancel() { 45 | s.cancel(); 46 | } 47 | }); 48 | } 49 | 50 | @Override 51 | public void onNext(Integer element) { 52 | super.onNext(element); 53 | probe.registerOnNext(element); 54 | } 55 | 56 | @Override 57 | public void onError(Throwable cause) { 58 | super.onError(cause); 59 | probe.registerOnError(cause); 60 | } 61 | 62 | @Override 63 | public void onComplete() { 64 | super.onComplete(); 65 | probe.registerOnComplete(); 66 | } 67 | 68 | @Override 69 | protected long foreach(Integer element) { 70 | return 1; 71 | } 72 | }; 73 | } 74 | 75 | @Override public Integer createElement(int element) { 76 | return element; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /tck-flow/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandFlowSubscriber.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import java.util.concurrent.Flow; 8 | 9 | /** 10 | * SyncTriggeredDemandSubscriber is an implementation of Reactive Streams `Subscriber`, 11 | * it runs synchronously (on the Publisher's thread) and requests demand triggered from 12 | * "the outside" using its `triggerDemand` method and from "the inside" using the return 13 | * value of its user-defined `whenNext` method which is invoked to process each element. 14 | * 15 | * NOTE: The code below uses a lot of try-catches to show the reader where exceptions can be expected, and where they are forbidden. 16 | */ 17 | // FIXME, depend on the reactive streams version? but that's in test scope... 18 | public abstract class SyncTriggeredDemandFlowSubscriber implements Flow.Subscriber { 19 | private Flow.Subscription subscription; // Obeying rule 3.1, we make this private! 20 | private boolean done = false; 21 | 22 | @Override public void onSubscribe(final Flow.Subscription s) { 23 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` 24 | if (s == null) throw null; 25 | 26 | if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully 27 | try { 28 | s.cancel(); // Cancel the additional subscription 29 | } catch(final Throwable t) { 30 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 31 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 32 | } 33 | } else { 34 | // We have to assign it locally before we use it, if we want to be a synchronous `Subscriber` 35 | // Because according to rule 3.10, the Subscription is allowed to call `onNext` synchronously from within `request` 36 | subscription = s; 37 | } 38 | } 39 | 40 | /** 41 | * Requests the provided number of elements from the `Subscription` of this `Subscriber`. 42 | * NOTE: This makes no attempt at thread safety so only invoke it once from the outside to initiate the demand. 43 | * @return `true` if successful and `false` if not (either due to no `Subscription` or due to exceptions thrown) 44 | */ 45 | public boolean triggerDemand(final long n) { 46 | final Flow.Subscription s = subscription; 47 | if (s == null) return false; 48 | else { 49 | try { 50 | s.request(n); 51 | } catch(final Throwable t) { 52 | // Subscription.request is not allowed to throw according to rule 3.16 53 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); 54 | return false; 55 | } 56 | return true; 57 | } 58 | } 59 | 60 | @Override public void onNext(final T element) { 61 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 62 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); 63 | } else { 64 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` 65 | if (element == null) throw null; 66 | 67 | if (!done) { // If we aren't already done 68 | try { 69 | final long need = foreach(element); 70 | if (need > 0) triggerDemand(need); 71 | else if (need == 0) {} 72 | else { 73 | done(); 74 | } 75 | } catch (final Throwable t) { 76 | done(); 77 | try { 78 | onError(t); 79 | } catch (final Throwable t2) { 80 | //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 81 | (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | // Showcases a convenience method to idempotently marking the Subscriber as "done", so we don't want to process more elements 89 | // herefor we also need to cancel our `Subscription`. 90 | private void done() { 91 | //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. 92 | done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) 93 | try { 94 | subscription.cancel(); // Cancel the subscription 95 | } catch(final Throwable t) { 96 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 97 | (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 98 | } 99 | } 100 | 101 | // This method is left as an exercise to the reader/extension point 102 | // Don't forget to call `triggerDemand` at the end if you are interested in more data, 103 | // a return value of < 0 indicates that the subscription should be cancelled, 104 | // a value of 0 indicates that there is no current need, 105 | // a value of > 0 indicates the current need. 106 | protected abstract long foreach(final T element); 107 | 108 | @Override public void onError(final Throwable t) { 109 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 110 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); 111 | } else { 112 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` 113 | if (t == null) throw null; 114 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 115 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 116 | } 117 | } 118 | 119 | @Override public void onComplete() { 120 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 121 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); 122 | } else { 123 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 124 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tck-flow/src/test/resources/testng.yaml: -------------------------------------------------------------------------------- 1 | name: TCKSuite 2 | threadCount: 1 3 | 4 | tests: 5 | - name: TCK 6 | classes: 7 | - org.reactivestreams.tck.IdentityProcessorVerificationDelegationTest 8 | - org.reactivestreams.tck.PublisherVerificationTest 9 | - org.reactivestreams.tck.SubscriberBlackboxVerificationTest 10 | - org.reactivestreams.tck.SubscriberWhiteboxVerificationTest 11 | -------------------------------------------------------------------------------- /tck/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'reactive-streams-tck' 2 | dependencies { 3 | api group: 'org.testng', name: 'testng', version:'7.3.0' 4 | api project(':reactive-streams') 5 | implementation project(':reactive-streams-examples') 6 | } 7 | 8 | jar { 9 | bnd ('Bundle-Name': 'reactive-streams-jvm', 10 | 'Bundle-Vendor': 'Reactive Streams SIG', 11 | 'Bundle-Description': 'Reactive Streams TCK', 12 | 'Bundle-DocURL': 'http://reactive-streams.org', 13 | 'Bundle-Version': project.version, 14 | 'Export-Package': 'org.reactivestreams.tck.*', 15 | 'Automatic-Module-Name': 'org.reactivestreams.tck', 16 | 'Bundle-SymbolicName': 'org.reactivestreams.tck' 17 | ) 18 | } 19 | 20 | test.useTestNG() 21 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/WithHelperPublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.tck.flow.support.Function; 9 | import org.reactivestreams.tck.flow.support.HelperPublisher; 10 | import org.reactivestreams.tck.flow.support.InfiniteHelperPublisher; 11 | 12 | import java.util.concurrent.ExecutorService; 13 | 14 | /** 15 | * Type which is able to create elements based on a seed {@code id} value. 16 | *

17 | * Simplest implementations will simply return the incoming id as the element. 18 | * 19 | * @param type of element to be delivered to the Subscriber 20 | */ 21 | public abstract class WithHelperPublisher { 22 | 23 | /** ExecutorService to be used by the provided helper {@link org.reactivestreams.Publisher} */ 24 | public abstract ExecutorService publisherExecutorService(); 25 | 26 | /** 27 | * Implement this method to match your expected element type. 28 | * In case of implementing a simple Subscriber which is able to consume any kind of element simply return the 29 | * incoming {@code element} element. 30 | *

31 | * Sometimes the Subscriber may be limited in what type of element it is able to consume, this you may have to implement 32 | * this method such that the emitted element matches the Subscribers requirements. Simplest implementations would be 33 | * to simply pass in the {@code element} as payload of your custom element, such as appending it to a String or other identifier. 34 | *

35 | * Warning: This method may be called concurrently by the helper publisher, thus it should be implemented in a 36 | * thread-safe manner. 37 | * 38 | * @return element of the matching type {@code T} that will be delivered to the tested Subscriber 39 | */ 40 | public abstract T createElement(int element); 41 | 42 | /** 43 | * Helper method required for creating the Publisher to which the tested Subscriber will be subscribed and tested against. 44 | *

45 | * By default an asynchronously signalling Publisher is provided, which will use {@link #createElement(int)} 46 | * to generate elements type your Subscriber is able to consume. 47 | *

48 | * Sometimes you may want to implement your own custom custom helper Publisher - to validate behaviour of a Subscriber 49 | * when facing a synchronous Publisher for example. If you do, it MUST emit the exact number of elements asked for 50 | * (via the {@code elements} parameter) and MUST also must treat the following numbers of elements in these specific ways: 51 | *

    52 | *
  • 53 | * If {@code elements} is {@code Long.MAX_VALUE} the produced stream must be infinite. 54 | *
  • 55 | *
  • 56 | * If {@code elements} is {@code 0} the {@code Publisher} should signal {@code onComplete} immediatly. 57 | * In other words, it should represent a "completed stream". 58 | *
  • 59 | *
60 | */ 61 | @SuppressWarnings("unchecked") 62 | public Publisher createHelperPublisher(long elements) { 63 | final Function mkElement = new Function() { 64 | @Override public T apply(Integer id) throws Throwable { 65 | return createElement(id); 66 | } 67 | }; 68 | 69 | if (elements > Integer.MAX_VALUE) return new InfiniteHelperPublisher(mkElement, publisherExecutorService()); 70 | else return new HelperPublisher(0, (int) elements, mkElement, publisherExecutorService()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/Function.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | public interface Function { 8 | public Out apply(In in) throws Throwable; 9 | } 10 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/HelperPublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import java.util.Collections; 8 | import java.util.Iterator; 9 | import java.util.concurrent.Executor; 10 | 11 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 12 | 13 | public class HelperPublisher extends AsyncIterablePublisher { 14 | 15 | public HelperPublisher(final int from, final int to, final Function create, final Executor executor) { 16 | super(new Iterable() { 17 | { if(from > to) throw new IllegalArgumentException("from must be equal or greater than to!"); } 18 | @Override public Iterator iterator() { 19 | return new Iterator() { 20 | private int at = from; 21 | @Override public boolean hasNext() { return at < to; } 22 | @Override public T next() { 23 | if (!hasNext()) return Collections.emptyList().iterator().next(); 24 | else try { 25 | return create.apply(at++); 26 | } catch (Throwable t) { 27 | throw new IllegalStateException(String.format("Failed to create element for id %d!", at - 1), t); 28 | } 29 | } 30 | @Override public void remove() { throw new UnsupportedOperationException(); } 31 | }; 32 | } 33 | }, executor); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/InfiniteHelperPublisher.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 8 | 9 | import java.util.Iterator; 10 | import java.util.concurrent.Executor; 11 | 12 | public class InfiniteHelperPublisher extends AsyncIterablePublisher { 13 | 14 | public InfiniteHelperPublisher(final Function create, final Executor executor) { 15 | super(new Iterable() { 16 | @Override public Iterator iterator() { 17 | return new Iterator() { 18 | private int at = 0; 19 | 20 | @Override public boolean hasNext() { return true; } 21 | @Override public T next() { 22 | try { 23 | return create.apply(at++); // Wraps around on overflow 24 | } catch (Throwable t) { 25 | throw new IllegalStateException( 26 | String.format("Failed to create element in %s for id %s!", getClass().getSimpleName(), at - 1), t); 27 | } 28 | } 29 | @Override public void remove() { throw new UnsupportedOperationException(); } 30 | }; 31 | } 32 | }, executor); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/NonFatal.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | 8 | /** 9 | * Copy of scala.control.util.NonFatal in order to not depend on scala-library 10 | */ 11 | public class NonFatal { 12 | private NonFatal() { 13 | // no instances, please. 14 | } 15 | 16 | /** 17 | * Returns true if the provided `Throwable` is to be considered non-fatal, or false if it is to be considered fatal 18 | * 19 | * @param t throwable to be matched for fatal-ness 20 | * @return true if is a non-fatal throwable, false otherwise 21 | */ 22 | public static boolean isNonFatal(Throwable t) { 23 | if (t instanceof StackOverflowError) { 24 | // StackOverflowError ok even though it is a VirtualMachineError 25 | return true; 26 | } else if (t instanceof VirtualMachineError || 27 | t instanceof ThreadDeath || 28 | t instanceof InterruptedException || 29 | t instanceof LinkageError) { 30 | // VirtualMachineError includes OutOfMemoryError and other fatal errors 31 | return false; 32 | } else { 33 | return true; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/Optional.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import java.util.NoSuchElementException; 8 | 9 | // simplest possible version of Scala's Option type 10 | public abstract class Optional { 11 | 12 | private static final Optional NONE = new Optional() { 13 | @Override 14 | public Object get() { 15 | throw new NoSuchElementException(".get call on None!"); 16 | } 17 | 18 | @Override 19 | public boolean isEmpty() { 20 | return true; 21 | } 22 | }; 23 | 24 | private Optional() { 25 | } 26 | 27 | @SuppressWarnings("unchecked") 28 | public static Optional empty() { 29 | return (Optional) NONE; 30 | } 31 | 32 | @SuppressWarnings("unchecked") 33 | public static Optional of(T it) { 34 | if (it == null) return (Optional) Optional.NONE; 35 | else return new Some(it); 36 | } 37 | 38 | public abstract T get(); 39 | 40 | public abstract boolean isEmpty(); 41 | 42 | public boolean isDefined() { 43 | return !isEmpty(); 44 | } 45 | 46 | public static class Some extends Optional { 47 | private final T value; 48 | 49 | Some(T value) { 50 | this.value = value; 51 | } 52 | 53 | @Override 54 | public T get() { 55 | return value; 56 | } 57 | 58 | @Override 59 | public boolean isEmpty() { 60 | return false; 61 | } 62 | 63 | @Override 64 | public String toString() { 65 | return String.format("Some(%s)", value); 66 | } 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | return "None"; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberBufferOverflowException.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | public final class SubscriberBufferOverflowException extends RuntimeException { 8 | public SubscriberBufferOverflowException() { 9 | } 10 | 11 | public SubscriberBufferOverflowException(String message) { 12 | super(message); 13 | } 14 | 15 | public SubscriberBufferOverflowException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | public SubscriberBufferOverflowException(Throwable cause) { 20 | super(cause); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/SubscriberWhiteboxVerificationRules.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | /** 8 | * Internal TCK use only. 9 | * Add / Remove tests for PublisherVerificaSubscriberWhiteboxVerification here to make sure that they arre added/removed in the other places. 10 | */ 11 | public interface SubscriberWhiteboxVerificationRules { 12 | void required_exerciseWhiteboxHappyPath() throws Throwable; 13 | void required_spec201_mustSignalDemandViaSubscriptionRequest() throws Throwable; 14 | void untested_spec202_shouldAsynchronouslyDispatch() throws Exception; 15 | void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnComplete() throws Throwable; 16 | void required_spec203_mustNotCallMethodsOnSubscriptionOrPublisherInOnError() throws Throwable; 17 | void untested_spec204_mustConsiderTheSubscriptionAsCancelledInAfterRecievingOnCompleteOrOnError() throws Exception; 18 | void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Throwable; 19 | void untested_spec206_mustCallSubscriptionCancelIfItIsNoLongerValid() throws Exception; 20 | void untested_spec207_mustEnsureAllCallsOnItsSubscriptionTakePlaceFromTheSameThreadOrTakeCareOfSynchronization() throws Exception; 21 | void required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable; 22 | void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable; 23 | void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable; 24 | void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable; 25 | void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall() throws Throwable; 26 | void untested_spec211_mustMakeSureThatAllCallsOnItsMethodsHappenBeforeTheProcessingOfTheRespectiveEvents() throws Exception; 27 | void untested_spec212_mustNotCallOnSubscribeMoreThanOnceBasedOnObjectEquality_specViolation() throws Throwable; 28 | void untested_spec213_failingOnSignalInvocation() throws Exception; 29 | void required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; 30 | void required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; 31 | void required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable; 32 | void untested_spec301_mustNotBeCalledOutsideSubscriberContext() throws Exception; 33 | void required_spec308_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable; 34 | void untested_spec310_requestMaySynchronouslyCallOnNextOnSubscriber() throws Exception; 35 | void untested_spec311_requestMaySynchronouslyCallOnCompleteOrOnError() throws Exception; 36 | void untested_spec314_cancelMayCauseThePublisherToShutdownIfNoOtherSubscriptionExists() throws Exception; 37 | void untested_spec315_cancelMustNotThrowExceptionAndMustSignalOnError() throws Exception; 38 | void untested_spec316_requestMustNotThrowExceptionAndMustOnErrorTheSubscriber() throws Exception; 39 | } 40 | -------------------------------------------------------------------------------- /tck/src/main/java/org/reactivestreams/tck/flow/support/TestException.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | /** 8 | * Exception used by the TCK to signal failures. 9 | * May be thrown or signalled through {@link org.reactivestreams.Subscriber#onError(Throwable)}. 10 | */ 11 | public final class TestException extends RuntimeException { 12 | public TestException() { 13 | super("Test Exception: Boom!"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/EmptyLazyPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 8 | import org.reactivestreams.Publisher; 9 | import org.testng.annotations.AfterClass; 10 | import org.testng.annotations.BeforeClass; 11 | import org.testng.annotations.Test; 12 | 13 | import java.util.Collections; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | 17 | @Test 18 | public class EmptyLazyPublisherTest extends PublisherVerification { 19 | 20 | private ExecutorService ex; 21 | 22 | public EmptyLazyPublisherTest() { 23 | super(new TestEnvironment()); 24 | } 25 | 26 | @BeforeClass 27 | void before() { ex = Executors.newFixedThreadPool(4); } 28 | 29 | @AfterClass 30 | void after() { if (ex != null) ex.shutdown(); } 31 | 32 | @Override 33 | public Publisher createPublisher(long elements) { 34 | return new AsyncIterablePublisher(Collections.emptyList(), ex); 35 | } 36 | 37 | @Override 38 | public Publisher createFailedPublisher() { 39 | return null; 40 | } 41 | 42 | @Override 43 | public long maxElementsFromPublisher() { 44 | return 0; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationDelegationTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.testng.annotations.Test; 8 | 9 | import java.lang.reflect.Method; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | 13 | import static org.testng.AssertJUnit.assertTrue; 14 | 15 | /** 16 | * The {@link org.reactivestreams.tck.IdentityProcessorVerification} must also run all tests from 17 | * {@link org.reactivestreams.tck.PublisherVerification} and {@link org.reactivestreams.tck.SubscriberWhiteboxVerification}. 18 | * 19 | * Since in Java this can be only achieved by delegating, we need to make sure we delegate to each of the tests, 20 | * so that if in the future we add more tests to these verifications we're sure to not forget to add the delegating methods. 21 | */ 22 | public class IdentityProcessorVerificationDelegationTest { 23 | 24 | @Test 25 | public void shouldIncludeAllTestsFromPublisherVerification() throws Exception { 26 | // given 27 | List processorTests = getTestNames(IdentityProcessorVerification.class); 28 | Class delegatedToClass = PublisherVerification.class; 29 | 30 | // when 31 | List publisherTests = getTestNames(delegatedToClass); 32 | 33 | // then 34 | assertSuiteDelegatedAllTests(IdentityProcessorVerification.class, processorTests, delegatedToClass, publisherTests); 35 | } 36 | 37 | @Test 38 | public void shouldIncludeAllTestsFromSubscriberVerification() throws Exception { 39 | // given 40 | List processorTests = getTestNames(IdentityProcessorVerification.class); 41 | Class delegatedToClass = SubscriberWhiteboxVerification.class; 42 | 43 | // when 44 | List publisherTests = getTestNames(delegatedToClass); 45 | 46 | // then 47 | assertSuiteDelegatedAllTests(IdentityProcessorVerification.class, processorTests, delegatedToClass, publisherTests); 48 | } 49 | 50 | private void assertSuiteDelegatedAllTests(Class delegatingFrom, List allTests, Class targetClass, List delegatedToTests) { 51 | for (String targetTest : delegatedToTests) { 52 | String msg = String.format( 53 | "Test '%s' in '%s' has not been properly delegated to in aggregate '%s'! \n" + 54 | "You must delegate to this test from %s, like this: \n" + 55 | "@Test public void %s() throws Exception { delegate%s.%s(); }", 56 | targetTest, targetClass, delegatingFrom, 57 | delegatingFrom, 58 | targetTest, targetClass.getSimpleName(), targetTest); 59 | 60 | assertTrue(msg, testsInclude(allTests, targetTest)); 61 | } 62 | } 63 | 64 | 65 | private boolean testsInclude(List processorTests, String publisherTest) { 66 | return processorTests.contains(publisherTest); 67 | } 68 | 69 | private List getTestNames(Class clazz) { 70 | List tests = new ArrayList(); 71 | for (Method method : clazz.getDeclaredMethods()) { 72 | if (method.isAnnotationPresent(Test.class)) { 73 | tests.add(method.getName()); 74 | } 75 | } 76 | 77 | return tests; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/IdentityProcessorVerificationTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.Processor; 8 | import org.reactivestreams.Publisher; 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.Subscription; 11 | import org.reactivestreams.tck.flow.support.TCKVerificationSupport; 12 | import org.testng.annotations.AfterClass; 13 | import org.testng.annotations.BeforeClass; 14 | import org.testng.annotations.Test; 15 | 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | /** 20 | * Validates that the TCK's {@link IdentityProcessorVerification} fails with nice human readable errors. 21 | * Important: Please note that all Processors implemented in this file are *wrong*! 22 | */ 23 | public class IdentityProcessorVerificationTest extends TCKVerificationSupport { 24 | 25 | static final long DEFAULT_TIMEOUT_MILLIS = TestEnvironment.envDefaultTimeoutMillis(); 26 | static final long DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS = TestEnvironment.envDefaultNoSignalsTimeoutMillis(); 27 | 28 | private ExecutorService ex; 29 | @BeforeClass void before() { ex = Executors.newFixedThreadPool(4); } 30 | @AfterClass void after() { if (ex != null) ex.shutdown(); } 31 | 32 | @Test 33 | public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError_shouldBeIgnored() throws Throwable { 34 | requireTestSkip(new ThrowingRunnable() { 35 | @Override public void run() throws Throwable { 36 | new IdentityProcessorVerification(newTestEnvironment(), DEFAULT_TIMEOUT_MILLIS){ 37 | @Override public Processor createIdentityProcessor(int bufferSize) { 38 | return new NoopProcessor(); 39 | } 40 | 41 | @Override public ExecutorService publisherExecutorService() { return ex; } 42 | 43 | @Override public Integer createElement(int element) { return element; } 44 | 45 | @Override public Publisher createHelperPublisher(long elements) { 46 | return SKIP; 47 | } 48 | 49 | @Override public Publisher createFailedPublisher() { 50 | return SKIP; 51 | } 52 | 53 | @Override public long maxSupportedSubscribers() { 54 | return 1; // can only support 1 subscribe => unable to run this test 55 | } 56 | }.required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError(); 57 | } 58 | }, "The Publisher under test only supports 1 subscribers, while this test requires at least 2 to run"); 59 | } 60 | 61 | @Test 62 | public void required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError_shouldFailWhileWaitingForOnError() throws Throwable { 63 | requireTestFailure(new ThrowingRunnable() { 64 | @Override public void run() throws Throwable { 65 | new IdentityProcessorVerification(newTestEnvironment(), DEFAULT_TIMEOUT_MILLIS) { 66 | @Override public Processor createIdentityProcessor(int bufferSize) { 67 | return new Processor() { 68 | @Override public void subscribe(final Subscriber s) { 69 | s.onSubscribe(new Subscription() { 70 | @Override public void request(long n) { 71 | s.onNext(0); 72 | } 73 | 74 | @Override public void cancel() { 75 | } 76 | }); 77 | } 78 | 79 | @Override public void onSubscribe(Subscription s) { 80 | s.request(1); 81 | } 82 | 83 | @Override public void onNext(Integer integer) { 84 | // noop 85 | } 86 | 87 | @Override public void onError(Throwable t) { 88 | // noop 89 | } 90 | 91 | @Override public void onComplete() { 92 | // noop 93 | } 94 | }; 95 | } 96 | 97 | @Override public ExecutorService publisherExecutorService() { return ex; } 98 | 99 | @Override public Integer createElement(int element) { return element; } 100 | 101 | @Override public Publisher createHelperPublisher(long elements) { 102 | return new Publisher() { 103 | @Override public void subscribe(final Subscriber s) { 104 | s.onSubscribe(new NoopSubscription() { 105 | @Override public void request(long n) { 106 | for (int i = 0; i < 10; i++) { 107 | s.onNext(i); 108 | } 109 | } 110 | }); 111 | } 112 | }; 113 | } 114 | 115 | @Override public Publisher createFailedPublisher() { 116 | return SKIP; 117 | } 118 | }.required_spec104_mustCallOnErrorOnAllItsSubscribersIfItEncountersANonRecoverableError(); 119 | } 120 | }, "Did not receive expected error on downstream within " + DEFAULT_TIMEOUT_MILLIS); 121 | } 122 | 123 | // FAILING IMPLEMENTATIONS // 124 | 125 | final Publisher SKIP = null; 126 | 127 | /** Subscription which does nothing. */ 128 | static class NoopSubscription implements Subscription { 129 | 130 | @Override public void request(long n) { 131 | // noop 132 | } 133 | 134 | @Override public void cancel() { 135 | // noop 136 | } 137 | } 138 | 139 | static class NoopProcessor implements Processor { 140 | 141 | @Override public void subscribe(Subscriber s) { 142 | s.onSubscribe(new NoopSubscription()); 143 | } 144 | 145 | @Override public void onSubscribe(Subscription s) { 146 | // noop 147 | } 148 | 149 | @Override public void onNext(Integer integer) { 150 | // noop 151 | } 152 | 153 | @Override public void onError(Throwable t) { 154 | // noop 155 | } 156 | 157 | @Override public void onComplete() { 158 | // noop 159 | } 160 | } 161 | 162 | private TestEnvironment newTestEnvironment() { 163 | return new TestEnvironment(DEFAULT_TIMEOUT_MILLIS, DEFAULT_NO_SIGNALS_TIMEOUT_MILLIS); 164 | } 165 | 166 | 167 | } 168 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/LockstepProcessorTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.*; 8 | import org.testng.annotations.Test; 9 | 10 | import java.util.concurrent.*; 11 | import java.util.concurrent.atomic.*; 12 | 13 | @Test 14 | public class LockstepProcessorTest extends IdentityProcessorVerification { 15 | 16 | public LockstepProcessorTest() { 17 | super(new TestEnvironment()); 18 | } 19 | @Override 20 | public Processor createIdentityProcessor(int bufferSize) { 21 | return new LockstepProcessor(); 22 | } 23 | 24 | @Override 25 | public Publisher createFailedPublisher() { 26 | LockstepProcessor proc = new LockstepProcessor(); 27 | proc.onError(new Exception()); 28 | return proc; 29 | } 30 | 31 | @Override 32 | public ExecutorService publisherExecutorService() { 33 | return Executors.newCachedThreadPool(); 34 | } 35 | 36 | @Override 37 | public Integer createElement(int element) { 38 | return element; 39 | } 40 | 41 | @Override 42 | public long maxSupportedSubscribers() { 43 | return 2; 44 | } 45 | 46 | @Override 47 | public boolean doesCoordinatedEmission() { 48 | return true; 49 | } 50 | 51 | static final class LockstepProcessor implements Processor { 52 | 53 | final AtomicReference[]> subscribers = 54 | new AtomicReference[]>(EMPTY); 55 | 56 | static final LockstepSubscription[] EMPTY = new LockstepSubscription[0]; 57 | static final LockstepSubscription[] TERMINATED = new LockstepSubscription[0]; 58 | 59 | volatile boolean done; 60 | Throwable error; 61 | 62 | final AtomicReference upstream = 63 | new AtomicReference(); 64 | 65 | final AtomicReferenceArray queue = 66 | new AtomicReferenceArray(BUFFER_MASK + 1); 67 | 68 | final AtomicLong producerIndex = new AtomicLong(); 69 | 70 | final AtomicLong consumerIndex = new AtomicLong(); 71 | 72 | final AtomicInteger wip = new AtomicInteger(); 73 | 74 | static final int BUFFER_MASK = 127; 75 | 76 | int consumed; 77 | 78 | @Override 79 | public void subscribe(Subscriber s) { 80 | LockstepSubscription subscription = new LockstepSubscription(s, this); 81 | s.onSubscribe(subscription); 82 | if (add(subscription)) { 83 | if (subscription.isCancelled()) { 84 | remove(subscription); 85 | } else { 86 | drain(); 87 | } 88 | } else { 89 | Throwable ex = error; 90 | if (ex != null) { 91 | s.onError(ex); 92 | } else { 93 | s.onComplete(); 94 | } 95 | } 96 | } 97 | 98 | boolean add(LockstepSubscription sub) { 99 | for (;;) { 100 | LockstepSubscription[] a = subscribers.get(); 101 | if (a == TERMINATED) { 102 | return false; 103 | } 104 | int n = a.length; 105 | LockstepSubscription[] b = new LockstepSubscription[n + 1]; 106 | System.arraycopy(a, 0, b, 0, n); 107 | b[n] = sub; 108 | if (subscribers.compareAndSet(a, b)) { 109 | return true; 110 | } 111 | } 112 | } 113 | 114 | void remove(LockstepSubscription sub) { 115 | for (;;) { 116 | LockstepSubscription[] a = subscribers.get(); 117 | int n = a.length; 118 | 119 | if (n == 0) { 120 | break; 121 | } 122 | 123 | int j = -1; 124 | for (int i = 0; i < n; i++) { 125 | if (a[i] == sub) { 126 | j = i; 127 | break; 128 | } 129 | } 130 | 131 | if (j < 0) { 132 | break; 133 | } 134 | LockstepSubscription[] b; 135 | if (n == 1) { 136 | b = TERMINATED; 137 | } else { 138 | b = new LockstepSubscription[n - 1]; 139 | System.arraycopy(a, 0, b, 0, j); 140 | System.arraycopy(a, j + 1, b, j, n - j - 1); 141 | } 142 | if (subscribers.compareAndSet(a, b)) { 143 | if (b == TERMINATED) { 144 | Subscription s = upstream.getAndSet(CancelledSubscription.INSTANCE); 145 | if (s != null) { 146 | s.cancel(); 147 | } 148 | } 149 | break; 150 | } 151 | } 152 | } 153 | 154 | @Override 155 | public void onSubscribe(Subscription s) { 156 | if (upstream.compareAndSet(null, s)) { 157 | s.request(BUFFER_MASK + 1); 158 | } else { 159 | s.cancel(); 160 | } 161 | } 162 | 163 | @Override 164 | public void onNext(T t) { 165 | if (t == null) { 166 | throw new NullPointerException("t == null"); 167 | } 168 | long pi = producerIndex.get(); 169 | queue.lazySet((int)pi & BUFFER_MASK, t); 170 | producerIndex.lazySet(pi + 1); 171 | drain(); 172 | } 173 | 174 | @Override 175 | public void onError(Throwable t) { 176 | if (t == null) { 177 | throw new NullPointerException("t == null"); 178 | } 179 | error = t; 180 | done = true; 181 | drain(); 182 | } 183 | 184 | @Override 185 | public void onComplete() { 186 | done = true; 187 | drain(); 188 | } 189 | 190 | void drain() { 191 | if (wip.getAndIncrement() != 0) { 192 | return; 193 | } 194 | 195 | int limit = (BUFFER_MASK + 1) - ((BUFFER_MASK + 1) >> 2); 196 | int missed = 1; 197 | for (;;) { 198 | 199 | for (;;) { 200 | LockstepSubscription[] subscribers = this.subscribers.get(); 201 | int n = subscribers.length; 202 | 203 | long ci = consumerIndex.get(); 204 | 205 | boolean d = done; 206 | boolean empty = producerIndex.get() == ci; 207 | 208 | if (d) { 209 | Throwable ex = error; 210 | if (ex != null) { 211 | for (LockstepSubscription sub : this.subscribers.getAndSet(TERMINATED)) { 212 | sub.subscriber.onError(ex); 213 | } 214 | break; 215 | } else if (empty) { 216 | for (LockstepSubscription sub : this.subscribers.getAndSet(TERMINATED)) { 217 | sub.subscriber.onComplete(); 218 | } 219 | break; 220 | } 221 | } 222 | 223 | if (n != 0 && !empty) { 224 | long ready = Long.MAX_VALUE; 225 | int c = 0; 226 | for (LockstepSubscription sub : subscribers) { 227 | long req = sub.get(); 228 | if (req != Long.MIN_VALUE) { 229 | ready = Math.min(ready, req - sub.emitted); 230 | c++; 231 | } 232 | } 233 | 234 | if (ready != 0 && c != 0) { 235 | int offset = (int) ci & BUFFER_MASK; 236 | T value = queue.get(offset); 237 | queue.lazySet(offset, null); 238 | consumerIndex.lazySet(ci + 1); 239 | 240 | for (LockstepSubscription sub : subscribers) { 241 | sub.subscriber.onNext(value); 242 | sub.emitted++; 243 | } 244 | 245 | if (++consumed == limit) { 246 | consumed = 0; 247 | upstream.get().request(limit); 248 | } 249 | } else { 250 | break; 251 | } 252 | } else { 253 | break; 254 | } 255 | } 256 | 257 | missed = wip.addAndGet(-missed); 258 | if (missed == 0) { 259 | break; 260 | } 261 | } 262 | } 263 | 264 | static final class LockstepSubscription extends AtomicLong 265 | implements Subscription { 266 | 267 | final Subscriber subscriber; 268 | 269 | final LockstepProcessor parent; 270 | 271 | long emitted; 272 | 273 | LockstepSubscription(Subscriber subscriber, LockstepProcessor parent) { 274 | this.subscriber = subscriber; 275 | this.parent = parent; 276 | } 277 | 278 | @Override 279 | public void request(long n) { 280 | if (n <= 0L) { 281 | cancel(); 282 | subscriber.onError(new IllegalArgumentException("§3.9 violated: positive request amount required")); 283 | return; 284 | } 285 | for (;;) { 286 | long current = get(); 287 | if (current == Long.MIN_VALUE || current == Long.MAX_VALUE) { 288 | break; 289 | } 290 | 291 | long updated = current + n; 292 | if (updated < 0L) { 293 | updated = Long.MAX_VALUE; 294 | } 295 | if (compareAndSet(current, updated)) { 296 | parent.drain(); 297 | break; 298 | } 299 | } 300 | } 301 | 302 | @Override 303 | public void cancel() { 304 | if (getAndSet(Long.MIN_VALUE) != Long.MIN_VALUE) { 305 | parent.remove(this); 306 | parent.drain(); 307 | } 308 | } 309 | 310 | boolean isCancelled() { 311 | return get() == Long.MIN_VALUE; 312 | } 313 | } 314 | } 315 | 316 | enum CancelledSubscription implements Subscription { 317 | 318 | INSTANCE; 319 | 320 | @Override 321 | public void request(long n) { 322 | // Subscription already cancelled 323 | } 324 | 325 | @Override 326 | public void cancel() { 327 | // Subscription already cancelled 328 | } 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/RangePublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | import java.util.concurrent.atomic.*; 10 | 11 | import org.reactivestreams.*; 12 | import org.testng.annotations.*; 13 | 14 | @Test 15 | public class RangePublisherTest extends PublisherVerification { 16 | 17 | static final Map stacks = new ConcurrentHashMap(); 18 | 19 | static final Map states = new ConcurrentHashMap(); 20 | 21 | static final AtomicInteger id = new AtomicInteger(); 22 | 23 | @AfterClass 24 | public static void afterClass() { 25 | boolean fail = false; 26 | StringBuilder b = new StringBuilder(); 27 | for (Map.Entry t : states.entrySet()) { 28 | if (!t.getValue()) { 29 | b.append("\r\n-------------------------------"); 30 | for (Object o : stacks.get(t.getKey())) { 31 | b.append("\r\nat ").append(o); 32 | } 33 | fail = true; 34 | } 35 | } 36 | if (fail) { 37 | throw new AssertionError("Cancellations were missing:" + b); 38 | } 39 | } 40 | 41 | public RangePublisherTest() { 42 | super(new TestEnvironment()); 43 | } 44 | 45 | @Override 46 | public Publisher createPublisher(long elements) { 47 | return new RangePublisher(1, elements); 48 | } 49 | 50 | @Override 51 | public Publisher createFailedPublisher() { 52 | return null; 53 | } 54 | 55 | static final class RangePublisher 56 | implements Publisher { 57 | 58 | final StackTraceElement[] stacktrace; 59 | 60 | final long start; 61 | 62 | final long count; 63 | 64 | RangePublisher(long start, long count) { 65 | this.stacktrace = Thread.currentThread().getStackTrace(); 66 | this.start = start; 67 | this.count = count; 68 | } 69 | 70 | @Override 71 | public void subscribe(Subscriber s) { 72 | if (s == null) { 73 | throw new NullPointerException(); 74 | } 75 | 76 | int ids = id.incrementAndGet(); 77 | 78 | RangeSubscription parent = new RangeSubscription(s, ids, start, start + count); 79 | stacks.put(ids, stacktrace); 80 | states.put(ids, false); 81 | s.onSubscribe(parent); 82 | } 83 | 84 | static final class RangeSubscription extends AtomicLong implements Subscription { 85 | 86 | private static final long serialVersionUID = 9066221863682220604L; 87 | 88 | final Subscriber actual; 89 | 90 | final int ids; 91 | 92 | final long end; 93 | 94 | long index; 95 | 96 | volatile boolean cancelled; 97 | 98 | RangeSubscription(Subscriber actual, int ids, long start, long end) { 99 | this.actual = actual; 100 | this.ids = ids; 101 | this.index = start; 102 | this.end = end; 103 | } 104 | 105 | @Override 106 | public void request(long n) { 107 | if (!cancelled) { 108 | if (n <= 0L) { 109 | cancelled = true; 110 | states.put(ids, true); 111 | actual.onError(new IllegalArgumentException("§3.9 violated")); 112 | return; 113 | } 114 | 115 | for (;;) { 116 | long r = get(); 117 | long u = r + n; 118 | if (u < 0L) { 119 | u = Long.MAX_VALUE; 120 | } 121 | if (compareAndSet(r, u)) { 122 | if (r == 0) { 123 | break; 124 | } 125 | return; 126 | } 127 | } 128 | 129 | long idx = index; 130 | long f = end; 131 | 132 | for (;;) { 133 | long e = 0; 134 | while (e != n && idx != f) { 135 | if (cancelled) { 136 | return; 137 | } 138 | 139 | actual.onNext((int)idx); 140 | 141 | idx++; 142 | e++; 143 | } 144 | 145 | if (idx == f) { 146 | if (!cancelled) { 147 | states.put(ids, true); 148 | actual.onComplete(); 149 | } 150 | return; 151 | } 152 | 153 | index = idx; 154 | n = addAndGet(-n); 155 | if (n == 0) { 156 | break; 157 | } 158 | } 159 | } 160 | } 161 | 162 | @Override 163 | public void cancel() { 164 | cancelled = true; 165 | states.put(ids, true); 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/SingleElementPublisherTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | import org.reactivestreams.example.unicast.AsyncIterablePublisher; 11 | import org.testng.annotations.AfterClass; 12 | import org.testng.annotations.BeforeClass; 13 | import org.testng.annotations.Test; 14 | 15 | import java.util.Collections; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.Executors; 18 | 19 | @Test 20 | public class SingleElementPublisherTest extends PublisherVerification { 21 | 22 | private ExecutorService ex; 23 | 24 | public SingleElementPublisherTest() { 25 | super(new TestEnvironment()); 26 | } 27 | 28 | @BeforeClass 29 | void before() { ex = Executors.newFixedThreadPool(4); } 30 | 31 | @AfterClass 32 | void after() { if (ex != null) ex.shutdown(); } 33 | 34 | @Override 35 | public Publisher createPublisher(long elements) { 36 | return new AsyncIterablePublisher(Collections.singleton(1), ex); 37 | } 38 | 39 | @Override 40 | public Publisher createFailedPublisher() { 41 | return null; 42 | } 43 | 44 | @Override 45 | public long maxElementsFromPublisher() { 46 | return 1; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/SubscriberNoRegisterOnSubscribeTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.*; 8 | import org.testng.annotations.Test; 9 | 10 | /** 11 | * This test verifies that the SubscriberWhiteboxVerification reports that 12 | * WhiteboxSubscriberProbe.registerOnSubscribe was not called during the setup 13 | * of each test. 14 | */ 15 | @Test 16 | public class SubscriberNoRegisterOnSubscribeTest extends SubscriberWhiteboxVerification { 17 | 18 | public SubscriberNoRegisterOnSubscribeTest() { 19 | super(new TestEnvironment()); 20 | } 21 | 22 | @Override 23 | public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { 24 | return new Subscriber() { 25 | @Override 26 | public void onSubscribe(final Subscription s) { 27 | // deliberately not calling probe.registerOnSubscribe() 28 | } 29 | 30 | @Override 31 | public void onNext(Integer integer) { 32 | probe.registerOnNext(integer); 33 | } 34 | 35 | @Override 36 | public void onError(Throwable t) { 37 | probe.registerOnError(t); 38 | } 39 | 40 | @Override 41 | public void onComplete() { 42 | probe.registerOnComplete(); 43 | } 44 | }; 45 | } 46 | 47 | @Override 48 | public Integer createElement(int element) { 49 | return element; 50 | } 51 | 52 | void assertMessage(AssertionError ex) { 53 | String message = ex.toString(); 54 | if (!message.contains(("did not `registerOnSubscribe` within"))) { 55 | throw ex; 56 | } 57 | } 58 | 59 | @Test 60 | @Override 61 | public void required_exerciseWhiteboxHappyPath() throws Throwable { 62 | try { 63 | super.required_exerciseWhiteboxHappyPath(); 64 | } catch (AssertionError ex) { 65 | assertMessage(ex); 66 | } 67 | } 68 | 69 | @Test 70 | @Override 71 | public void required_spec201_mustSignalDemandViaSubscriptionRequest() throws Throwable { 72 | try { 73 | super.required_spec201_mustSignalDemandViaSubscriptionRequest(); 74 | } catch (AssertionError ex) { 75 | assertMessage(ex); 76 | } 77 | } 78 | 79 | @Test 80 | @Override 81 | public void required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal() throws Throwable { 82 | try { 83 | super.required_spec205_mustCallSubscriptionCancelIfItAlreadyHasAnSubscriptionAndReceivesAnotherOnSubscribeSignal(); 84 | } catch (AssertionError ex) { 85 | assertMessage(ex); 86 | } 87 | } 88 | 89 | @Test 90 | @Override 91 | public void required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel() throws Throwable { 92 | try { 93 | super.required_spec208_mustBePreparedToReceiveOnNextSignalsAfterHavingCalledSubscriptionCancel(); 94 | } catch (AssertionError ex) { 95 | assertMessage(ex); 96 | } 97 | } 98 | 99 | @Test 100 | @Override 101 | public void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall() throws Throwable { 102 | try { 103 | super.required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithoutPrecedingRequestCall(); 104 | } catch (AssertionError ex) { 105 | assertMessage(ex); 106 | } 107 | } 108 | 109 | @Test 110 | @Override 111 | public void required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall() throws Throwable { 112 | try { 113 | super.required_spec209_mustBePreparedToReceiveAnOnCompleteSignalWithPrecedingRequestCall(); 114 | } catch (AssertionError ex) { 115 | assertMessage(ex); 116 | } 117 | } 118 | 119 | @Test 120 | @Override 121 | public void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall() throws Throwable { 122 | try { 123 | super.required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithoutPrecedingRequestCall(); 124 | } catch (AssertionError ex) { 125 | assertMessage(ex); 126 | } 127 | } 128 | 129 | @Test 130 | @Override 131 | public void required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall() throws Throwable { 132 | try { 133 | super.required_spec210_mustBePreparedToReceiveAnOnErrorSignalWithPrecedingRequestCall(); 134 | } catch (AssertionError ex) { 135 | assertMessage(ex); 136 | } 137 | } 138 | 139 | @Test 140 | @Override 141 | public void required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { 142 | try { 143 | super.required_spec213_onError_mustThrowNullPointerExceptionWhenParametersAreNull(); 144 | } catch (AssertionError ex) { 145 | assertMessage(ex); 146 | } 147 | } 148 | 149 | @Test 150 | @Override 151 | public void required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { 152 | try { 153 | super.required_spec213_onNext_mustThrowNullPointerExceptionWhenParametersAreNull(); 154 | } catch (AssertionError ex) { 155 | assertMessage(ex); 156 | } 157 | } 158 | 159 | @Test 160 | @Override 161 | public void required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull() throws Throwable { 162 | try { 163 | super.required_spec213_onSubscribe_mustThrowNullPointerExceptionWhenParametersAreNull(); 164 | } catch (AssertionError ex) { 165 | assertMessage(ex); 166 | } 167 | } 168 | 169 | @Test 170 | @Override 171 | public void required_spec308_requestMustRegisterGivenNumberElementsToBeProduced() throws Throwable { 172 | try { 173 | super.required_spec308_requestMustRegisterGivenNumberElementsToBeProduced(); 174 | } catch (AssertionError ex) { 175 | assertMessage(ex); 176 | } 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import java.util.concurrent.*; 8 | 9 | import org.reactivestreams.Subscriber; 10 | import org.reactivestreams.tck.flow.support.SyncTriggeredDemandSubscriber; 11 | import org.testng.annotations.*; 12 | 13 | @Test // Must be here for TestNG to find and run this, do not remove 14 | public class SyncTriggeredDemandSubscriberTest extends SubscriberBlackboxVerification { 15 | 16 | private ExecutorService e; 17 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 18 | @AfterClass void after() { if (e != null) e.shutdown(); } 19 | 20 | public SyncTriggeredDemandSubscriberTest() { 21 | super(new TestEnvironment()); 22 | } 23 | 24 | @Test(enabled = false) // TestNG tries to inject here but this is a helper method 25 | @Override public void triggerRequest(final Subscriber subscriber) { 26 | ((SyncTriggeredDemandSubscriber)subscriber).triggerDemand(1); 27 | } 28 | 29 | @Override public Subscriber createSubscriber() { 30 | return new SyncTriggeredDemandSubscriber() { 31 | private long acc; 32 | @Override protected long foreach(final Integer element) { 33 | acc += element; 34 | return 1; 35 | } 36 | 37 | @Override public void onComplete() { 38 | } 39 | }; 40 | } 41 | 42 | @Override public Integer createElement(int element) { 43 | return element; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/SyncTriggeredDemandSubscriberWhiteboxTest.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | import org.testng.annotations.AfterClass; 10 | import org.testng.annotations.BeforeClass; 11 | import org.testng.annotations.Test; 12 | 13 | import org.reactivestreams.tck.flow.support.SyncTriggeredDemandSubscriber; 14 | 15 | import java.util.concurrent.ExecutorService; 16 | import java.util.concurrent.Executors; 17 | 18 | @Test // Must be here for TestNG to find and run this, do not remove 19 | public class SyncTriggeredDemandSubscriberWhiteboxTest extends SubscriberWhiteboxVerification { 20 | 21 | private ExecutorService e; 22 | @BeforeClass void before() { e = Executors.newFixedThreadPool(4); } 23 | @AfterClass void after() { if (e != null) e.shutdown(); } 24 | 25 | public SyncTriggeredDemandSubscriberWhiteboxTest() { 26 | super(new TestEnvironment()); 27 | } 28 | 29 | @Override 30 | public Subscriber createSubscriber(final WhiteboxSubscriberProbe probe) { 31 | return new SyncTriggeredDemandSubscriber() { 32 | @Override 33 | public void onSubscribe(final Subscription s) { 34 | super.onSubscribe(s); 35 | 36 | probe.registerOnSubscribe(new SubscriberPuppet() { 37 | @Override 38 | public void triggerRequest(long elements) { 39 | s.request(elements); 40 | } 41 | 42 | @Override 43 | public void signalCancel() { 44 | s.cancel(); 45 | } 46 | }); 47 | } 48 | 49 | @Override 50 | public void onNext(Integer element) { 51 | super.onNext(element); 52 | probe.registerOnNext(element); 53 | } 54 | 55 | @Override 56 | public void onError(Throwable cause) { 57 | super.onError(cause); 58 | probe.registerOnError(cause); 59 | } 60 | 61 | @Override 62 | public void onComplete() { 63 | super.onComplete(); 64 | probe.registerOnComplete(); 65 | } 66 | 67 | @Override 68 | protected long foreach(Integer element) { 69 | return 1; 70 | } 71 | }; 72 | } 73 | 74 | @Override public Integer createElement(int element) { 75 | return element; 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/flow/support/SyncTriggeredDemandSubscriber.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import org.reactivestreams.Subscriber; 8 | import org.reactivestreams.Subscription; 9 | 10 | /** 11 | * SyncTriggeredDemandSubscriber is an implementation of Reactive Streams `Subscriber`, 12 | * it runs synchronously (on the Publisher's thread) and requests demand triggered from 13 | * "the outside" using its `triggerDemand` method and from "the inside" using the return 14 | * value of its user-defined `whenNext` method which is invoked to process each element. 15 | * 16 | * NOTE: The code below uses a lot of try-catches to show the reader where exceptions can be expected, and where they are forbidden. 17 | */ 18 | public abstract class SyncTriggeredDemandSubscriber implements Subscriber { 19 | private Subscription subscription; // Obeying rule 3.1, we make this private! 20 | private boolean done = false; 21 | 22 | @Override public void onSubscribe(final Subscription s) { 23 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Subscription` is `null` 24 | if (s == null) throw null; 25 | 26 | if (subscription != null) { // If someone has made a mistake and added this Subscriber multiple times, let's handle it gracefully 27 | try { 28 | s.cancel(); // Cancel the additional subscription 29 | } catch(final Throwable t) { 30 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 31 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 32 | } 33 | } else { 34 | // We have to assign it locally before we use it, if we want to be a synchronous `Subscriber` 35 | // Because according to rule 3.10, the Subscription is allowed to call `onNext` synchronously from within `request` 36 | subscription = s; 37 | } 38 | } 39 | 40 | /** 41 | * Requests the provided number of elements from the `Subscription` of this `Subscriber`. 42 | * NOTE: This makes no attempt at thread safety so only invoke it once from the outside to initiate the demand. 43 | * @return `true` if successful and `false` if not (either due to no `Subscription` or due to exceptions thrown) 44 | */ 45 | public boolean triggerDemand(final long n) { 46 | final Subscription s = subscription; 47 | if (s == null) return false; 48 | else { 49 | try { 50 | s.request(n); 51 | } catch(final Throwable t) { 52 | // Subscription.request is not allowed to throw according to rule 3.16 53 | (new IllegalStateException(s + " violated the Reactive Streams rule 3.16 by throwing an exception from request.", t)).printStackTrace(System.err); 54 | return false; 55 | } 56 | return true; 57 | } 58 | } 59 | 60 | @Override public void onNext(final T element) { 61 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 62 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onNext prior to onSubscribe.")).printStackTrace(System.err); 63 | } else { 64 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `element` is `null` 65 | if (element == null) throw null; 66 | 67 | if (!done) { // If we aren't already done 68 | try { 69 | final long need = foreach(element); 70 | if (need > 0) triggerDemand(need); 71 | else if (need == 0) {} 72 | else { 73 | done(); 74 | } 75 | } catch (final Throwable t) { 76 | done(); 77 | try { 78 | onError(t); 79 | } catch (final Throwable t2) { 80 | //Subscriber.onError is not allowed to throw an exception, according to rule 2.13 81 | (new IllegalStateException(this + " violated the Reactive Streams rule 2.13 by throwing an exception from onError.", t2)).printStackTrace(System.err); 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | // Showcases a convenience method to idempotently marking the Subscriber as "done", so we don't want to process more elements 89 | // herefor we also need to cancel our `Subscription`. 90 | private void done() { 91 | //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. 92 | done = true; // If we `whenNext` throws an exception, let's consider ourselves done (not accepting more elements) 93 | try { 94 | subscription.cancel(); // Cancel the subscription 95 | } catch(final Throwable t) { 96 | //Subscription.cancel is not allowed to throw an exception, according to rule 3.15 97 | (new IllegalStateException(subscription + " violated the Reactive Streams rule 3.15 by throwing an exception from cancel.", t)).printStackTrace(System.err); 98 | } 99 | } 100 | 101 | // This method is left as an exercise to the reader/extension point 102 | // Don't forget to call `triggerDemand` at the end if you are interested in more data, 103 | // a return value of < 0 indicates that the subscription should be cancelled, 104 | // a value of 0 indicates that there is no current need, 105 | // a value of > 0 indicates the current need. 106 | protected abstract long foreach(final T element); 107 | 108 | @Override public void onError(final Throwable t) { 109 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 110 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onError prior to onSubscribe.")).printStackTrace(System.err); 111 | } else { 112 | // As per rule 2.13, we need to throw a `java.lang.NullPointerException` if the `Throwable` is `null` 113 | if (t == null) throw null; 114 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 115 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 116 | } 117 | } 118 | 119 | @Override public void onComplete() { 120 | if (subscription == null) { // Technically this check is not needed, since we are expecting Publishers to conform to the spec 121 | (new IllegalStateException("Publisher violated the Reactive Streams rule 1.09 signalling onComplete prior to onSubscribe.")).printStackTrace(System.err); 122 | } else { 123 | // Here we are not allowed to call any methods on the `Subscription` or the `Publisher`, as per rule 2.3 124 | // And anyway, the `Subscription` is considered to be cancelled if this method gets called, as per rule 2.4 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tck/src/test/java/org/reactivestreams/tck/flow/support/TCKVerificationSupport.java: -------------------------------------------------------------------------------- 1 | /*************************************************** 2 | * Licensed under MIT No Attribution (SPDX: MIT-0) * 3 | ***************************************************/ 4 | 5 | package org.reactivestreams.tck.flow.support; 6 | 7 | import org.reactivestreams.Publisher; 8 | import org.reactivestreams.Subscriber; 9 | import org.reactivestreams.Subscription; 10 | import org.testng.SkipException; 11 | 12 | import java.util.concurrent.atomic.AtomicBoolean; 13 | import java.util.concurrent.atomic.AtomicLong; 14 | 15 | import static org.testng.Assert.fail; 16 | 17 | /** 18 | * Provides assertions to validate the TCK tests themselves, 19 | * with the goal of guaranteeing proper error messages when an implementation does not pass a given TCK test. 20 | * 21 | * "Quis custodiet ipsos custodes?" -- Iuvenalis 22 | */ 23 | public class TCKVerificationSupport { 24 | 25 | // INTERNAL ASSERTION METHODS // 26 | 27 | /** 28 | * Runs given code block and expects it to fail with an "Expected onError" failure. 29 | * Use this method to validate that TCK tests fail with meaningful errors instead of NullPointerExceptions etc. 30 | * 31 | * @param run encapsulates test case which we expect to fail 32 | * @param msgPart the exception failing the test (inside the run parameter) must contain this message part in one of it's causes 33 | */ 34 | public void requireTestFailure(ThrowingRunnable run, String msgPart) { 35 | try { 36 | run.run(); 37 | } catch (Throwable throwable) { 38 | if (findDeepErrorMessage(throwable, msgPart)) { 39 | return; 40 | } else { 41 | throw new RuntimeException( 42 | String.format("Expected TCK to fail with '... %s ...', " + 43 | "yet `%s(%s)` was thrown and test would fail with not useful error message!", 44 | msgPart, throwable.getClass().getName(), throwable.getMessage()), throwable); 45 | } 46 | } 47 | throw new RuntimeException(String.format("Expected TCK to fail with '... %s ...', " + 48 | "yet no exception was thrown and test would pass unexpectedly!", 49 | msgPart)); 50 | } 51 | 52 | /** 53 | * Runs given code block and expects it fail with an {@code org.testng.SkipException} 54 | * 55 | * @param run encapsulates test case which we expect to be skipped 56 | * @param msgPart the exception failing the test (inside the run parameter) must contain this message part in one of it's causes 57 | */ 58 | public void requireTestSkip(ThrowingRunnable run, String msgPart) { 59 | try { 60 | run.run(); 61 | } catch (SkipException skip) { 62 | if (skip.getMessage().contains(msgPart)) { 63 | return; 64 | } else { 65 | throw new RuntimeException( 66 | String.format("Expected TCK to skip this test with '... %s ...', yet it skipped with (%s) instead!", 67 | msgPart, skip.getMessage()), skip); 68 | } 69 | } catch (Throwable throwable) { 70 | throw new RuntimeException( 71 | String.format("Expected TCK to skip this test, yet it threw %s(%s) instead!", 72 | throwable.getClass().getName(), throwable.getMessage()), throwable); 73 | } 74 | throw new RuntimeException("Expected TCK to SKIP this test, instead if PASSed!"); 75 | } 76 | 77 | public void requireOptionalTestPass(ThrowingRunnable run) { 78 | try { 79 | run.run(); 80 | } catch (SkipException skip) { 81 | throw new RuntimeException("Expected TCK to PASS this test, instead it was SKIPPED", skip.getCause()); 82 | } catch (Throwable throwable) { 83 | throw new RuntimeException( 84 | String.format("Expected TCK to PASS this test, yet it threw %s(%s) instead!", 85 | throwable.getClass().getName(), throwable.getMessage()), throwable); 86 | } 87 | } 88 | 89 | /** 90 | * This publisher does NOT fulfil all Publisher spec requirements. 91 | * It's just the bare minimum to enable this test to fail the Subscriber tests. 92 | */ 93 | public Publisher newSimpleIntsPublisher(final long maxElementsToEmit) { 94 | return new Publisher() { 95 | @Override public void subscribe(final Subscriber s) { 96 | s.onSubscribe(new Subscription() { 97 | private AtomicLong nums = new AtomicLong(); 98 | private AtomicBoolean active = new AtomicBoolean(true); 99 | 100 | @Override public void request(long n) { 101 | long thisDemand = n; 102 | while (active.get() && thisDemand > 0 && nums.get() < maxElementsToEmit) { 103 | s.onNext((int) nums.getAndIncrement()); 104 | thisDemand--; 105 | } 106 | 107 | if (nums.get() == maxElementsToEmit) { 108 | s.onComplete(); 109 | } 110 | } 111 | 112 | @Override public void cancel() { 113 | active.set(false); 114 | } 115 | }); 116 | } 117 | }; 118 | } 119 | 120 | /** 121 | * Looks for expected error message prefix inside of causes of thrown throwable. 122 | * 123 | * @return true if one of the causes indeed contains expected error, false otherwise 124 | */ 125 | public boolean findDeepErrorMessage(Throwable throwable, String msgPart) { 126 | return findDeepErrorMessage(throwable, msgPart, 5); 127 | } 128 | 129 | private boolean findDeepErrorMessage(Throwable throwable, String msgPart, int depth) { 130 | if (throwable instanceof NullPointerException) { 131 | fail(String.format("%s was thrown, definitely not a helpful error!", NullPointerException.class.getName()), throwable); 132 | return false; 133 | } else if (throwable == null || depth == 0) { 134 | return false; 135 | } else { 136 | final String message = throwable.getMessage(); 137 | return (message != null && message.contains(msgPart)) || findDeepErrorMessage(throwable.getCause(), msgPart, depth - 1); 138 | } 139 | } 140 | 141 | /** Like {@link java.lang.Runnable} but allows throwing {@link java.lang.Throwable}, useful for wrapping test execution */ 142 | public static interface ThrowingRunnable { 143 | void run() throws Throwable; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tck/src/test/resources/testng.yaml: -------------------------------------------------------------------------------- 1 | name: TCKSuite 2 | threadCount: 1 3 | 4 | tests: 5 | - name: TCK 6 | classes: 7 | - org.reactivestreams.tck.IdentityProcessorVerificationDelegationTest 8 | - org.reactivestreams.tck.PublisherVerificationTest 9 | - org.reactivestreams.tck.SubscriberBlackboxVerificationTest 10 | - org.reactivestreams.tck.SubscriberWhiteboxVerificationTest 11 | --------------------------------------------------------------------------------